class Flows::Contract::Compose

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 {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](math.stackexchange.com/questions/600978/equivalence-relation-composition-problem).

Public Class Methods

new(*contracts) click to toggle source

@param contracts [Array<Contract, Object>] contract list. Non-contract elements will be wrapped with {CaseEq}.

# 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

Public Instance Methods

check!(other) click to toggle source

@see Contract#check!

# File lib/flows/contract/compose.rb, line 64
def check!(other)
  @contracts.each { |con| con.check!(other) }
  true
end
transform!(other) click to toggle source

@see Contract#transform!

# File lib/flows/contract/compose.rb, line 70
def transform!(other)
  @contracts.reduce(other) do |value, con|
    con.transform!(value)
  end
end