Class: Flows::Contract::Compose

Inherits:
Flows::Contract show all
Defined in:
lib/flows/contract/compose.rb

Overview

Allows to combine two or more contracts.

From type system perspective - this composition is intersection of types. It means that value passes contract if it passes each particular contract in a composition.

Composition and Transform Laws

Golden rule: don't use contracts with transformations in composition if you can. In the most cases you can compose contracts without transformations and apply one transformation to composite contract.

Composition of contracts' transformations MUST obey Transform Laws (see Flows::Contract documentation for details). To achieve this each particular transform MUST obey following additional laws:

# let `c` be a contract composition

# 1. each transform should not leave composite type
#
# for any `x` valid for composite type
c.check!(x) == true
# and for any contract `c_i` from composition:
c.check!(c_i.transform!(x)) == true

# 2. tranforms can be applied in any order
#
# for any `x` valid for composite type
c.check!(x) == true
# for any two contracts `c_i` and `c_j` from composition:
c_i(c_j(x)) == c_j(c_i(x))

Why do we need the first law? To prevent situations when original value matches composite type, but transformed value doesn't. Example:

Flows::Contract.make do
  compose(
    transform(either(String, Symbol), &:to_sym),
    String
  )
end

Second laws makes composition of transforms to obey 2nd transform law. Example of correct composable transforms:

Flows::Contract.make do
  compose(
    transform(String, &:strip),
    transform(String, &:trim)
  )
end

Formal proof is based on this theorem proof.

Since:

  • 0.4.0

Instance Method Summary collapse

Methods inherited from Flows::Contract

#===, #check, make, #to_proc, #transform

Constructor Details

#initialize(*contracts) ⇒ Compose

Returns a new instance of Compose.

Parameters:

Since:

  • 0.4.0



57
58
59
60
61
# File 'lib/flows/contract/compose.rb', line 57

def initialize(*contracts)
  raise 'Contract list must not be empty' if contracts.length.zero?

  @contracts = contracts.map(&method(:to_contract))
end

Instance Method Details

#check!(other) ⇒ Object

See Also:

Since:

  • 0.4.0



64
65
66
67
# File 'lib/flows/contract/compose.rb', line 64

def check!(other)
  @contracts.each { |con| con.check!(other) }
  true
end

#transform!(other) ⇒ Object

See Also:

Since:

  • 0.4.0



70
71
72
73
74
# File 'lib/flows/contract/compose.rb', line 70

def transform!(other)
  @contracts.reduce(other) do |value, con|
    con.transform!(value)
  end
end