Method: ADT#cases

Defined in:
lib/adt.rb

#cases(&definitions) ⇒ Object (private)

Configures the class to be an ADT. Cases are defined by calling methods named for the case, and providing symbol arguments for the parameters to the case.

eg.

class Validation
  extend ADT
  cases do
    success(:values)
    failure(:errors, :position)
  end
end

This will provde 2 core pieces of functionality.

  1. Constructors, as class methods, named the same as the case, and expecting parameters as per the symbol arguments provided in the ‘cases` block.

    @failure = Validation.failure(["error1"], 5)
    @success = Validation.success([1,2])
    
  2. #fold. This method takes a proc for every case. If the case has parameters, those will be passed to the proc. The proc matching the particular value of the case will be called. Using this method, every instance method for the ADT can be defined.

    @failure.fold(
      proc { |values| "We are a success! #{values} "},
      proc { |errors, position| "Failed :(, at position #{position}" }
    )
    

    It can also be passed a hash of procs, keyed by case name:

    @failure.fold(
      :success => proc { |values| values },
      :failures => proc { |errors, position| [] }
    )
    

In addition, a number of helper methods are defined:

  • Standard object methods: #==, #inspect

  • Conversion to an array of the arguments: #to_a (nullary constructors return empty arrays)

  • #<=> and Comparable: cases are compared by index, and then by their parameters as an array

  • Case checking predicates:

    some_validation.success?
    some_validation.failure?
    
  • Functions for handling specific cases:

    some_validation.when_success(proc { |values| values }, proc { [] })
    
  • Case information

    some_validation.case_name # <= "success"
    some_validation.case_index # <= 1 # Indexing is 1-based.
    some_validation.case_arity # <= 1 # Number of arguments required by the case
    
  • #fold is aliased to an underscored name of the type. ie. ValidatedValue gets #validated_value

If the type is an enumeration (it only has nullary constructors), then a few extra methods are available:

  • 1-based conversion to and from integers: #to_i, ::from_i

  • Accessor for all values: ::all_values

Parameters:

  • &definitions (Proc)

    block which defines the constructors. This will be evaluated using #instance_eval to record the cases.



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/adt.rb', line 76

def cases(&definitions)
  singleton_class = class <<self; self; end
  dsl = CaseRecorder.new
  dsl.__instance_eval(&definitions)

  cases = dsl._church_cases
  num_cases = dsl._church_cases.length
  case_names = dsl._church_cases.map { |x| x[0] }
  is_enumeration = dsl._church_cases.all?{ |(_, args)| args.count == 0 }

  # creates procs with a certain arg count. body should use #{prefix}N to access arguments. The result should be
  # eval'ed at the call site
  proc_create = proc { |argc, prefix, body|
    args = argc > 0 ? "|#{(1..argc).to_a.map { |a| "#{prefix}#{a}" }.join(',')}|" : ""
    "proc { #{args} #{body} }" 
  }

  # Initializer. Should not be used directly.
  define_method(:initialize) do |&fold|
    @fold = fold
  end

  # The Fold.
  define_method(:fold) do |*args|
    if args.first && args.first.is_a?(Hash) then
      @fold.call(*case_names.map { |cn| args.first.fetch(cn) })
    else
      @fold.call(*args)
    end
  end

  # If we're inside a named class, then set up an alias to fold
  define_method(StringHelp.underscore(name.split('::').last)) do |*args| fold(*args) end

  # The Constructors
  dsl._church_cases.each_with_index do |(name, case_args), index|
    constructor = proc { |*args| self.new(&eval(proc_create[num_cases, "a", "a#{index+1}.call(*args)"])) }
    if case_args.size > 0 then
      singleton_class.send(:define_method, name, &constructor)
    else
      # Cache the constructed value if it is unary
      singleton_class.send(:define_method, name) do
        instance_variable_get("@#{name}") || begin
          instance_variable_set("@#{name}", constructor.call)
        end
      end
    end
  end

  # Case info methods
  # Indexing is 1-based
  define_method(:case_index) do fold(*(1..case_names.length).to_a.map { |i| proc { i } }) end
  define_method(:case_name) do fold(*case_names.map { |i| proc { i.to_s } }) end
  define_method(:case_arity) do fold(*dsl._church_cases.map { |(_, args)| proc { args.count } }) end

  # Enumerations are defined as classes with cases that don't take arguments. A number of useful
  # functions can be defined for these.
  if is_enumeration 
    singleton_class.send(:define_method, :all_values) do
      @all_values ||= case_names.map { |x| send(x) }
    end

    define_method(:to_i) { case_index }
    singleton_class.send(:define_method, :from_i) do |idx| send(case_names[idx - 1]) end
  end

  # The usual object helpers
  define_method(:inspect) do
    "#<" + self.class.name + fold(*dsl._church_cases.map { |(cn, case_args)|
      index = 0
      bit = case_args.map { |ca| 
        index += 1
        " #{ca}:#\{a#{index}\}"
      }.join('')
      eval(proc_create[case_args.count, "a", " \" #{cn}#{bit}\""])
    }) + ">"
  end

  define_method(:==) do |other|
    !other.nil? && case_index == other.case_index && to_a == other.to_a
  end

  define_method(:to_a) do 
    fold(*cases.map { |(cn, args)|
      eval(proc_create[args.count, "a", "[" + (1..args.count).to_a.map { |idx| "a#{idx}" }.join(',') + "]"])
    })
  end

  # Comparisons are done by index, then by the values within the case (if any) via #to_a
  define_method(:<=>) do |other|
    comp = case_index <=> other.case_index
    comp == 0 ?  to_a <=> other.to_a : comp
  end
  include Comparable

  # Case specific methods
  # eg.
  #     cases do foo(:a); bar(:b); end
  cases.each_with_index do |(name, args), idx|
    #     Thing.foo(5).foo? # <= true
    #     Thing.foo(5).bar? # <= false
    define_method("#{name}?") do
      fold(*case_names.map { |cn|
        eval(proc_create[0, "a", cn == name ? "true" : "false"])
      })
    end
    
    #     Thing.foo(5).when_foo(proc {|v| v }, proc { 0 }) # <= 5
    #     Thing.bar(5).when_foo(proc {|v| v }, proc { 0 }) # <= 0
    define_method("when_#{name}") do |handle, default|
      fold(*case_names.map { |cn| 
        if (cn == name)
         proc { |*args| handle.call(*args) }
        else
         default
        end
      })
    end
  end
end