Skip to content

Instantly share code, notes, and snippets.

@mklbtz
Last active September 2, 2017 20:57
Show Gist options
  • Save mklbtz/78789343ce9f3de2cb977b5bf58f2517 to your computer and use it in GitHub Desktop.
Save mklbtz/78789343ce9f3de2cb977b5bf58f2517 to your computer and use it in GitHub Desktop.
Ruby Enums plus example
# A Numeric-like class that can represent algebraic expressions.
# Uses typecases to define what operations are representable.
class Algebra
include Enum
# Represents number literals
typecase :scalar, value: Numeric do
def to_s
value.to_s
end
def to_i
value.to_i
end
def to_f
value.to_f
end
end
# Like a Scalar, but has a name. Useful for nesting expressions.
typecase :variable, name: String, value: Algebra do
def to_s
name
end
def to_i
value.to_i
end
def to_f
value.to_f
end
end
typecase :add, lhs: Algebra, rhs: Algebra do
def to_s
"(#{lhs} + #{rhs})"
end
def to_i
lhs.to_i + rhs.to_i
end
def to_f
lhs.to_f + rhs.to_f
end
end
typecase :negate, value: Algebra do
def to_s
if value.is_a? Negate
"#{value.value}" # simplify double negation
else
"–#{value}"
end
end
def to_i
-value.to_i
end
def to_f
-value.to_f
end
end
typecase :multiply, lhs: Algebra, rhs: Algebra do
def to_s
"#{lhs} × #{rhs}"
end
def to_i
lhs.to_i * rhs.to_i
end
def to_f
lhs.to_f * rhs.to_f
end
end
typecase :divide, dividend: Algebra, divisor: Algebra do
def to_s
"(#{dividend}) ÷ (#{divisor})"
end
def to_i
dividend.to_i / divisor.to_i
end
def to_f
dividend.to_f / divisor.to_f
end
end
def + other
Algebra.add(self, Algebra(other))
end
def -@
Algebra.negate(self)
end
def - other
Algebra.add(self, -Algebra(other))
end
def * other
Algebra.multiply(self, Algebra(other))
end
end
def Algebra(other)
case other
when Algebra
other
else
Algebra.scalar(other)
end
end
secret = Algebra.variable("secret", Algebra(42))
expr = (Algebra(23) - 2) * secret + 23
expr.to_i
# => 905
expr.to_s
# => "((23 + –2) × secret + 23)"
expr.inspect
# => "Add(lhs: Multiply(lhs: Add(lhs: Scalar(value: 23), rhs: Negate(value: Scalar(value: 2))), rhs: Variable(name: \"secret\", value: Scalar(value: 42))), rhs: Scalar(value: 23))"
# Include in classes that should behave like enums in Swift or Rust.
# These are also known as "sum types" in type theory.
# Using the `typecase` method, create Struct-like subclassses with type-checked attributes.
# Methods can be added to subclasses using standard syntax or by passing a block to `typecase`.
module Enum
def self.included(base)
base.extend(ClassMethods)
base.include(InstanceMethods)
end
module ClassMethods
def typecase(name, **attrs, &block)
Enum::Constructor.new(self, name.capitalize, attrs, &block).child_class
end
end
module InstanceMethods
def initialize
raise "#{self.class} is abstract, use a subclass constructor instead"
end
def attributes
self.class.attribute_names.map do |name|
[name, public_send(name)]
end.to_h
end
def to_s
class_name = self.class.name.split('::').last
attr_descriptions = attributes.map do |name, value|
"#{name}: #{value}"
end.join(', ')
"#{class_name}(#{attr_descriptions})"
end
def inspect
class_name = self.class.name.split('::').last
attr_descriptions = attributes.map do |name, value|
"#{name}: #{value.inspect}"
end.join(', ')
"#{class_name}(#{attr_descriptions})"
end
def [] attr_name
public_send(attr_name)
end
def == other
other.is_a?(self.class) && attribute_names.all? { |name|
public_send(name) == other.public_send(name)
}
end
private
def validate_arguments(args)
arg_types = args.map(&:class)
wrong_types = attribute_names
.zip(attribute_types, arg_types)
.reject do |_, expected, actual|
return false if actual.nil?
actual <= expected
end
unless wrong_types.empty?
raise TypeError, error_messages(wrong_types).join(', ')
end
end
def error_messages(list)
list.map do |name, expected, actual|
"#{self.class}##{name} expected #{expected}, got #{actual}"
end
end
end
class Constructor
attr_reader :child_class
def initialize(parent, child_name, child_attrs, &block)
@child_class = new_subclass(parent, child_name)
define_attr_methods(@child_class, child_attrs.keys, child_attrs.values)
@child_class.class_eval(&block) if block
end
private
def new_subclass(parent, class_name)
child = Class.new(parent)
parent.const_set(class_name, child)
method_name = class_name.downcase
parent.define_singleton_method(method_name) do |*args|
const_get(class_name).new(*args)
end
parent.class_eval do
define_method("#{method_name}?") { self.is_a? child }
end
child.class_eval do
define_method(:initialize) do |*args|
validate_arguments(args)
attribute_names.zip(args).each do |name, arg|
instance_variable_set("@#{name}", arg)
end
end
end
child
end
def define_attr_methods(child, attr_names, attr_types)
child.define_singleton_method(:attribute_names) { attr_names }
child.class_eval do
define_method(:attribute_names) { attr_names }
define_method(:attribute_types) { attr_types }
private(:attribute_types)
attr_names.each { |name| attr_reader(name) }
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment