Neiro | Functional programming, software architecture
24 Okt 2015

Type checking ruby with contracts

Ruby is dynamically and strong typed programming language. In the most of the cases it gives you required level of type safety with minimal code. But if you want build more secure applications or you’re like static typing, then you need to check every variable or method for it’s type or class:

def foo(bar)
  fail 'I supposed it`s not a bar!'
  unless bar.is_a?(String) p 'Hi, bar!'
end

foo 'bar' # Hi, bar!
foo 100_500 # RuntimeError: I supposed it`s not a bar!

But what if you’re needing more complex type checking on multiple types or conditions? Then you need to provide more boilerplate, defensive code. If you’re want to make your type safety code much cleaner, there is the contracts library.

1 Contracts

What is a contract? It’s a pattern, that comes from functional programming world. In most cases this is one line of code before function or method, that validates the arguments and validates return value.

For example, there is a simple contract:

require 'contracts'

class Square
  include Contracts::Core
  include Contracts::Builtin

  Contract Num => Num
  def self.area(a) a**2 end
end

Square.area 10 # 100
Square.area 'a' # ParamContractError: Contract violation for argument 1 of 1
Square.area [] # ParamContractError: Contract violation for argument 1 of 1

You can also use it on multiple arguments or returns:

class Rectangle
  include Contracts::Core
  include Contracts::Builtin

  Contract Num, Num => Num
  def self.area(a, b)
    a * b
  end
end

Rectangle.area 10, 10 # 100
Rectangle.area [], false # ParamContractError: Contract violation for argument 1 of 2
Rectangle.area 10, 'a' # ParamContractError: Contract violation for argument 2 of 2

If you don’t want to throw exception, you can easily override error callback:

Contract.override_failure_callback do |data|
  puts 'IT`S AN OM~ ERROR!1'
  p data
end

Rectangle.area 10, 'a' # 'IT`S AN OM~ ERROR!1'

2 Custom types

Contracts library comes with many built-in type contracts:

  • Basic types: Num, Pos, Neg, Nat, Bool, Any, None
  • Logical: Maybe, Or, Xor, And, Not
  • Collections: ArrayOf, SetOf, HashOf, RangeOf, Enum

and others. But if your want to create your own types or check more complex conditions, then you have to use lambdas:

class CharCounter
  include Contracts::Core
  include Contracts::Builtin

  Char = -> (char) { char.is_a?(String) && char.length == 1 && char =~ /\w/ }

  Contract Maybe[String], Char => Num

  def self.count_chars(str, ch)
    str.count ch
    end
end

CharCounter.count_chars 'hello', 'N' # 0
CharCounter.count_chars 'hello', 'l' # 2
CharCounter.count_chars 'hello', '*' # ParamContractError: Contract violation for argument 2 of 2
CharCounter.count_chars 'llo', 'llo' # ParamContractError: Contract violation for argument 2 of 2

3 Pattern matching

Pattern matching, like a contract, comes from functional programming. You can use your contracts to test if your method matches pattern or not. For example, let’s find a factorial of number with contracts:

#+beginsrc ruby class Factorial include Contracts::Core include Contracts::Builtin

Contract 0 => 1 def self.factorial(n) 1 end

Contract Num => Num def self.factorial(n) n * factorial(n - 1) end end

Factorial.factorial 0 # 0 Factorial.factorial 10 # 3628800 Factorial.factorial ’a’ # ContractError: Contract violation for argument 1 of 1 #+endsrc

4 Conclusion

Ruby has simple and powerful type system, but if it’s not enough or you want to use safety type checking and you don’t like to write tons of a defensive code, then you may like Contracts library. Contracts allows you to check many types, conditions for your class methods much cleaner and simpler. Also you can define your own types or conditions with plain Ruby lambdas, and then use them for pattern-matching.

If you’re like it and want to know more, there is Ruby contracts tutorial.

Tags: :ruby:contracts:functional:typing