RobertDober/lab42_data_class

View on GitHub
speculations/BUILTIN_CONSTRAINTS.md

Summary

Maintainability
Test Coverage

## Context: Builtin Constraints


Given some setup
```ruby
    require "lab42/data_class/builtin_constraints"

    let(:constraint_error) { Lab42::DataClass::ConstraintError }
```
### Calling Convention

At first please note that all Builtins that take _exactly_ one constraint
as a paramter can also take a block, the block is registered as a constraint and
**not** it's value

Then surprisingly
```ruby
    bad_constraint = DataClass(:value).with_constraint(value: NilOr{Set.new(%w[x y z])})
    expect{ bad_constraint.new(value: "a") }.not_to raise_error # BUT SHOULD
```

But the following (useless complicated) works:
```ruby
    complicated_constraint = DataClass(:value).with_constraint(value: NilOr{ Set.new(%w[x y z]).member? _1 })
    expect{ complicated_constraint.new(value: "a") }.to raise_error(constraint_error) # BUT SHOULD
```

### Enumerable Constraints

#### Context: `All?(constraint)`

> :warning: This might be costly

Given a dataclass with an array value
```ruby
    let(:listy) { DataClass(value: []).with_constraint(value: All?(:odd?)) }
```
Then we are ok if we obey ;)
```ruby
    expect(listy.new.merge(value: [1, 3]).value).to eq([1, 3])
```

But beware of evens
```ruby
    expect{ listy.new(value: [2]) }
      .to raise_error(constraint_error)
```

#### Context: `Any?(constraint)`

> :warning: This might be costly

This works analogously

Given a dataclass with a hash_value
```ruby
    let(:hashy) { DataClass(:value).with_constraint(value: Any?{|(_,v)| v == 42}) }
```

Then we need a value of 42, of course
```ruby
    expect(hashy.new(value: {a: 42}).value[:a]).to eq(42)
```

And if not, beware
```ruby
    expect{ hashy.new(value: {a: 41, b: 43}) }
      .to raise_error(constraint_error)
```

#### Context: `ListOf(constraint)`

Not only is the constraint a convenient way to validate for linked lists and their contents but
it generates special merge operations that can validate the constraint in `O(1)` runtime as the
tail of the list does not change

Given a `DataClass` with such a constraint
```ruby
    let(:evens) { DataClass(list: Nil, name: "myself").with_constraint(list: ListOf(:even?)) }
```

And we construct such a list (`O(n)` anyway):
```ruby
    let(:fours) { evens.new(list: List(*(1..3).map{ _1 * 4 })) }
```

Then we can just add a new element to such a list
```ruby
    with_0 = fours.set(:list).cons(0)
    expect(with_0.list.car).to eq(0)
    expect(with_0.list.cadr).to eq(4)
    expect(with_0.list.caddr).to eq(8)
```

Or we can remove it
```ruby
    without_4 = fours.set(:list).cdr
    expect(without_4.list.car).to eq(8)
    expect(without_4.list.cadr).to eq(12)
    expect(without_4.list.cddr).to eq(Nil)
```

But we cannot call the setter for a different attribute
```ruby
    expect do
      fours.set(:name)
    end
      .to raise_error(Lab42::DataClass::UndefinedSetterError)
```

But a list with an odd will just not do
```ruby
    expect do
      evens.new(list: List(*0..2))
    end
      .to raise_error(constraint_error)
```

And nor will an array

```ruby
    expect do
      evens.new(list: [0, 2])
    end
      .to raise_error(constraint_error)
```


#### Context: `PairOf(fst, snd)`

When defining constraints on our special classes we can dig deeper

Given such a constraint
```ruby
    let(:my_pair){ DataClass(:pair).with_constraint(pair: PairOf(Symbol, [:>, 0])) }
```

Then let us comply with that
```ruby
    expect(my_pair.new(pair: Pair(:alpha, 42)).pair.second).to eq(42)
```

But there are multiple ways to violate this contract
```ruby
    not_a_pair = Triple(:hello, 42, 42)
    not_a_sym = Pair("hello", 1)
    not_positive = Pair(:hello, 0)

    [not_a_pair, not_a_sym, not_positive].each do |culprit|
      expect{my_pair.new(pair: culprit)}
        .to raise_error(constraint_error)
    end
```

For the Attribute Setting Constraints see [here](ATTRIBUTE_SETTING_CONSTRAINTS.md)

#### Context: `TripleOf(fst, snd, trd)`

Nothing new going on here

Given
```ruby
    let(:my_triple) { DataClass(:triple).with_constraint(triple: TripleOf(String, String, String)) }
```

Then strings it shall be
```ruby
    expect{ my_triple.new(triple: Triple("a", "b", "c")) }.not_to raise_error
```

But else
```ruby
    [
      Triple("a", "b", 3),
      Triple("a", 2, "c"),
      Triple(1, "b", "c")
      ].each do |culprit|
      expect{my_triple.new(triple: culprit)}
        .to raise_error(constraint_error)
    end


```

For the Attribute Setting Constraints see [here](ATTRIBUTE_SETTING_CONSTRAINTS.md)

### High Order Constraints

#### Context: NilOr(constraint)

NilOr resambles a `Maybe` type in which `Some` is not stated and `None` is replaced by `nil`.
Therefore a `nil` value is valid and all other values are valid iff they satisfy the constraint.

Given such a constraint
```ruby
    let(:maybe) { DataClass(number: nil).with_constraint(number: NilOr(Fixnum)) }
```

**N.B.** That constraints are checked against _default values_ and therefore the constraint `Fixnum` was not
even applicable under this definition

Then we get
```ruby
    expect(maybe.new.number).to be_nil
    expect(maybe.new(number: 42).number).to eq(42)
    expect{ maybe.new(number: false) }.to raise_error(constraint_error)
```

#### Context: `Not(constraint)`

Not (pun intended) to say here

And therefore
```ruby
    negator = DataClass(:consonant).with_constraint(consonant: Not(Set.new(%w[a e i o u])))
    expect(negator.new(consonant: "b").consonant).to eq("b")
    expect{ negator.new(consonant: "a") }.to raise_error(constraint_error)
```
#### Context: `Choice(*constraints)`

Given a choice between Strings and Symbols as keys we might model as follows
```ruby
    let(:key_constraint) { Choice(String, Symbol) }
    let(:entry) { DataClass(:value).with_constraint(value: PairOf(key_constraint, Anything)) }
```

Then the following holds:
```ruby
    entry.new(value: Pair("hello", "world"))
    entry.new(value: Pair(:hello, 42))
```

But indeed:
```ruby
    expect{ entry.new(value: Pair(42, "hello")) }.to raise_error(constraint_error)
```

#### Context: `Lambda(arity)`

Actually `Lambda` stands for a _callable_ with the given `arity`

Then we see that the following callables are compliant
```ruby
    callable1 = Lambda(1)
    compliants = [
      -> { _1 },
      1.method(:+) ]
    non_compliants = [
      -> { nil },
      42,
      Set.method(:new) # arity -1
      ]
    compliants.each do |compliant|
      expect(callable1.(compliant)).to be_truthy
    end
    non_compliants.each do |culprit|
      expect(callable1.(culprit)).to be_falsy
    end
```

### Context: String Constraints

Given these builtins
```ruby
    let(:starts) { StartsWith("<") }
    let(:ends)   { EndsWith(">") }
    let(:container) { Contains("div") }
```

Then we can check
```ruby
    expect(starts.("<hello")).to be_truthy
    expect(starts.("<")).to be_truthy
    expect(starts.(" ")).to be_falsy
    expect(starts.("")).to be_falsy
    expect(starts.(" <@")).to be_falsy
```

And also
```ruby
    expect(ends.(">")).to be_truthy
    expect(ends.("<hello>")).to be_truthy
    expect(ends.("")).to be_falsy
    expect(ends.("> ")).to be_falsy
```

And eventually
```ruby
    expect(container.("div")).to be_truthy
    expect(container.("some <div>")).to be_truthy
    expect(container.("some <dv>")).to be_falsy
    expect(container.("")).to be_falsy
```


### Miscellaneous

#### Context: `Anything`

This has already been shown (bad practice, but should be obvious)

And we see
```ruby
    expect(Anything.(nil)).to eq(true)
    expect(Anything.(42)).to eq(true)
    expect(Anything.(BasicObject.new)).to eq(true)
    expect(Anything.(self)).to eq(true)
```

#### Context: `Boolean`

As we do not have such a type in Ruby this constraint is quite useful

And it is true for exactly two values
```ruby
    expect(Boolean.(true)).to eq(true)
    expect(Boolean.(false)).to eq(true)
    expect(Boolean.(nil)).to eq(false)
    expect(Boolean.(42)).to eq(false)
    expect(Boolean.([])).to eq(false)
    expect(Boolean.([false])).to eq(false)

```
<!--SPDX-License-Identifier: Apache-2.0-->