Last active
September 2, 2017 20:57
-
-
Save mklbtz/78789343ce9f3de2cb977b5bf58f2517 to your computer and use it in GitHub Desktop.
Ruby Enums plus example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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))" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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