0.0
No release in over a year
A compact DSL to let you declare sum data types and define safe functions on them.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

~> 5.15
~> 13.0.6
~> 1.26
~> 1.0.0
 Project Readme

Unit tests

Safe and Sound: Sum Data Types and utilities for Ruby

This library gives you and alternative syntax to declare new types/classes. It's inspired by the concise syntax to declare new types in Elm or Haskell These types share some properties with types referred to as algebaric data types, sum types or union types.

We can model similar relationships more verbosely in plain Ruby with classes and subclasses. This library provides some syntactic shortcuts to create these hierarchies.

Vehicle = SafeAndSound.new(
    Car: { horsepower: Integer },
    Bike: { gears: Integer}
  )

This will create an abstract base class Vehicle. Instances can only be created for the concrete subclasses Car or Bike. The class names act as "constructor" functions and values created that way are immutable.

car = Vehicle.car(horsepower: 100)
puts car.horsepower # 100

bike = Vehicle.bike(gears: 'twentyone')
# SafeAndSound::WrgonConstructorArgType (gears must be of type Integer but was String)

nil is not a valid constructor argument. Optional values can be modeled with the Maybe type that is also provided with the library.

To add polymorphic behavior we can write functions without having to touch the new types themselves.

Safe, polymorphic functions

By including the SafeAndSound::Functions module we get access to the chase function. It immitates the case statement but uses the knowledge about our types to make it more safe.

include SafeAndSound::Functions

def to_human(vehicle)
  chase vehicle do
    wenn Vehicle::Car,  -> { "I'm a car with #{horsepower} horsepower" }
    wenn Vehicle::Bike, -> { "I'm a bike with #{gears} gears" }
  end
end

This offers a stricter version of the case statement. Specifically it makes sure that all variants are handled (unless an otherwise block is given). This check will still be only performed at runtime, but as long as there is at least one test executing this chase expression we'll get an early, precise exception telling us what's missing.

If you want a more detailed explanation why working with such objects can be appealing I recommend you watch the Functional Core, Imperative Shell episode of the Destroy all software screencast.

I'm not trying to change how Ruby code is written. This is merely an experiment how far the sum type concept can be taken in terms of making a syntax for it look like the syntax in languages where this concept is more central.

Check out more examples in the examples folder.

JSON serialization/deserialization included

irb(main)> car = Vehicle.Car(horsepower: 100)

irb(main)> car.as_json # converts to a Hash of primitives
=> {"type"=>"Car", "horsepower"=>100}

irb(main)> puts car.to_json # converts to actual JSON string
{"type":"Car","horsepower":100}

irb(main)> Vehicle.from_hash({"type"=>"Car", "horsepower"=>100})
=> #<Vehicle::Car:0x000000010ef49a48 @horsepower=100>

irb(main)> Vehicle.from_json('{"type":"Car","horsepower":100}')
=> #<Vehicle::Car:0x000000010ef6ae00 @horsepower=100>