Ducks and Monads

Wonders of Ruby Types

Hey, I'm Igor

/'iːgɔːɹ/

I'm a polyglot programmer at Qlean.ru

I don't trust myself

There are four reasons I don't trust myself

undefined is not a function

N + 1

I mess up business logic

I forget to close a bracket

I forget some details

I have to double-check my code

New hobby: finding the right tools

Tools to write type-safe Ruby code

Dynamic typing is not an excuse to be reckless

2 out of 10 most common errors are about types

Silent data corruption:
JSON in Rails

    
      value = "{\"foo\":\"bar\"}"

      # Rails 4
      my_model.value = value # => { foo: "bar" }

      # Rails 5
      my_model.value = value # => "{\"foo\":\"bar\"}"
    

  

We have to use the right libraries

Writing code is easier than fixing it

Every object has a type

What's a type, anyway?

  • Syntactic
  • Representation
  • Representation and behavior
  • Value space
  • Value space and behavior

A type is a set of values which a variable can possess and a set of functions that one can apply to these values.

We learn to consider all possible values

nil does not belong to every type

ActiveSupport teaches us to be reckless

blank? and present?
are bad for your code

We have to think about design of our data

Being careful is not enough

dry-types is a simple and extendable type system for Ruby

Data constructors

    
      Types::Strict::String["foo"] # => "foo"
      Types::Strict::String[nil] # => Dry::Types::ConstraintError:
      # nil violates constraints

      Types::Optional::Strict::String[nil] # => nil
    

  

Complex definitions

    
      IntArray = Types::Strict::Array.of(Types::Strict::Integer)

      Enum = Types::Strict::String.enum('foo', 'bar')

      Timestamp = Types::Strict::String | Types::Strict::Time

      MyHash = Types::Strict::Hash.schema(
        foo: Types.Instance(MyClass),
        bar: Types::Strict::Float
      )
    

  

dry-types won't change your architecture

Trying out dry-types: Hanami

Enhancing your application with dry-types

  • Describe your data using dry-types
  • Use those definitions in constructors
  • Use them in setters
  • (hard) Use dry-struct for domain models

Three steps to type safety

  • Stop using ActiveSupport
  • Consider all possible values
  • Use strict constructors

Expressing errors with types

Errors are not that exceptional

Types can facilitate error handling

Monads in Ruby

Monads are similar to musical instruments

A monad is a result object with a few rules

Result (Either) monad

type Result a b = Failure a | Success b

Using Result

    
      require 'dry/monads/result'

      include Dry::Monads::Result

      Success(1).success # => 1
      Success(1).success? # => true

      Failure("abc").failure # => "abc"
      Failure("abc").failure? # => true
    

  

Computations: bind

    
      Success(1).bind do |value|
        if value.positive?
          Failure("Welp")
        else
          Success(value + 3)
        end
      end # => Failure("Welp")
    

  

Handling errors: or

    
      Failure(:not_found).or do |error|
        case error
        when :not_found
          Failure(:db_error)
        else
          Success("Yay!")
        end
      end # => Failure(:db_error)
    

  

Non-monadic computations: fmap

    
      Success(1).fmap do |value|
        "The value is #{value}"
      end # => Success("The value is 1")

      Failure(1).fmap do |value|
        "The value is #{value}"
      end # => Failure(1)
    

  

Escaping the context:
value_or

    
      Success(-5).bind do |x|
        if x.positive?
          Success(x)
        else
          Failure("Something went wrong")
        end
      end.value_or { nil } # => nil
    

  

Unwrapping

    
      Failure(
        "Something went wrong"
      ).value_or(&:itself) # => "Something went wrong"
    

  

Do notation

    
      class Operation
        include Dry::Monads::Do.for(:call)
        include Dry::Monads::Result

        def call(name)
          user = yield find_by_name(name)

          contract = yield find_contract(user)

          Success(contract.new(
            amount: contract.amount + 100
          ))
        end
      end
    

  

Recap

Types help us design our applications

ActiveSupport may be bad for our code

Use dry-types for a type-safe application design

Monads help us handle errors gracefully

Useful links and references

Thank you! ❤

I would love to hear from you
[email protected]