contracts.ruby icon indicating copy to clipboard operation
contracts.ruby copied to clipboard

Contracts on `self`

Open vlad-shatskyi opened this issue 9 years ago • 28 comments

Let's say I have the following contracts

Contract Alkohol, Underaged => String
def sell(drink, person)
  "Sorry, come back in #{18 - person.age} years."
end

Contract Drink, Person => String
def sell(drink, person)
  "Here's your #{drink}."
end

full code

It works perfectly well, but let's say I want to define the sell method on Person. Now it becomes not obvious how would I check if the person is allowed to have the drink.

I propose to have some sort of self constraints. In Haskell-like syntax it would be

Contract :: (Underaged self) => Drink -> String

vlad-shatskyi avatar Jul 29 '15 07:07 vlad-shatskyi

Just like that (normal OO way):

class Underaged
  include Contracts::Core

  Contract Drink => String
  def sell(drink)
    "Sorry, come back in #{18 - person.age} years."
  end
end

class Person
  include Contracts::Core

  Contract Drink => String
  def sell(drink)
    "Here is your drink."
  end
end

waterlink avatar Jul 29 '15 07:07 waterlink

@waterlink, now I'd need a factory to create a person depending on age. You propose to use polymorphism, whereas the gem advertises to enable methods overloading, which I'm trying to achieve. I understand that normally OO languages don't dispatch based on self, but why not?

Update: I guess it's not correct to say that OO languages don't dispatch based on self. If you have only one criteria for dispatching - a type - there is nothing to choose from. Quite easy to pick one from a set of one, right? We, however, have more ways to describe a contract, and that raises a need for the feature I propose.

vlad-shatskyi avatar Jul 29 '15 07:07 vlad-shatskyi

Thing is you already have a Underaged defined in your example.

nixpulvis avatar Jul 29 '15 07:07 nixpulvis

I do have an Underaged contract, but it's a constraint, not a class, which I'd use for polymorphic behaviour.

vlad-shatskyi avatar Jul 29 '15 07:07 vlad-shatskyi

I'm willing to implement the feature providing we agree on the syntax and its usefulness.

vlad-shatskyi avatar Jul 29 '15 08:07 vlad-shatskyi

Propose a syntax then :)

Other way: just use private method, that doesn't depend on self. Like this:

class Person
  Contract Drink => String
  def sell(drink)
    _sell(self, drink)
  end

  private

  Contract Underaged, Drink => String
  def _sell(person, drink)
    "Sorry, come back in #{18 - person.age} years."
  end

  Contract Person, Drink => String
  def _sell(person, drink)
    "Here is your #{drink}!"
  end
end

alex-fedorov avatar Jul 29 '15 10:07 alex-fedorov

I'm afraid I don't have enough knowledge about which symbols can be used. Since you chose => to delimit arguments from the return value, I guess -> is not possible. Maybe something like

Contract Alkohol > Underaged => String
Contract Alkohol | Underaged => String
Contract Alkohol >> Underaged => String

I hope you'll agree with me that the workaround is super-ugly.

vlad-shatskyi avatar Jul 29 '15 10:07 vlad-shatskyi

How about:

Contract Contracts::Receiver[Underaged], Drink => String

?

alex-fedorov avatar Jul 29 '15 13:07 alex-fedorov

You know better, but won't it cause difficulties to distinguish whether the contract is for the first parameter or for the receiver?

vlad-shatskyi avatar Jul 29 '15 13:07 vlad-shatskyi

It will cause this problem in any case. On Jul 29, 2015 3:04 PM, "Volodymyr Shatsky" [email protected] wrote:

You know better, but won't it cause difficulties to distinguish whether the contract is for the first parameter or for the receiver?

— Reply to this email directly or view it on GitHub https://github.com/egonSchiele/contracts.ruby/issues/191#issuecomment-125943552 .

alex-fedorov avatar Jul 29 '15 17:07 alex-fedorov

I mean you chose the same separator as for the parameters list. If the separator was different, it would be easier to tell the difference.

vlad-shatskyi avatar Jul 29 '15 18:07 vlad-shatskyi

This does sound like a useful feature. Maybe we could do

Contract(Underaged) Drink => String

and

Contract(Person) Drink => String

If we have that code, it will build the foundation for future changes, where we can pass in arbitrary data to the Contract function before specifying the contract itself.

egonSchiele avatar Jul 29 '15 18:07 egonSchiele

Unfortunately, this is not a valid ruby. To make it valid we will have to send some message after calling Contract(x):

Contract(Underaged).with Drink => String

waterlink avatar Jul 29 '15 20:07 waterlink

Another example would be to return a lambda and call it:

Contract(Underaged).(Drink => String)
# or
Contract(Underaged)[Drink => String]
# or
Contract[Underaged].(Drink => String)
# or
Contract[Underaged][Drink => String]

waterlink avatar Jul 29 '15 20:07 waterlink

Some interesting syntax still can be used:

Contract[Underaged].>> Drink => String

waterlink avatar Jul 29 '15 20:07 waterlink

Last one resembles Haskell-like version the most.

waterlink avatar Jul 29 '15 20:07 waterlink

Yeah that's what I was thinking @waterlink...Contract(Underage) would return a callable object. Then Contract(Underage) foo => bar would be like calling that function with foo => bar.

I'm also ok with the >> syntax:

Contract Underage >> Foo => Bar

But I thought that might get confusing and start to look like symbol soup. @shockone once we agree on a syntax a PR for this would be great!

egonSchiele avatar Jul 29 '15 20:07 egonSchiele

@egonSchiele This does not work, I have tried it before. Even if Contract(Underage) returns callable object, ruby parser can't handle something(args) more, args. And callable objects (such as method, proc, block and lambda) in ruby can be called only with:

  • #call, ie: callable.call(args) or callable.(args)
  • #[], ie: callable[args]

waterlink avatar Jul 29 '15 21:07 waterlink

@egonSchiele What about using a word instead of a symbol, ie: Contract(Underage).with Foo => Bar? Or any other short word, which means the same: "Underage with args: Foo and returns Bar".

waterlink avatar Jul 29 '15 21:07 waterlink

Yeah, I like with too, but would we be able to keep existing syntax? Like Contract Num => Num?

egonSchiele avatar Jul 29 '15 21:07 egonSchiele

Not a problem. We need to switch on fact if there is a hash in arguments to Contract or not. If yes: it is an old syntax and we just instantiate contract object right from the bet. If no: we return contract factory object, that have #with or #>> method.

waterlink avatar Jul 29 '15 21:07 waterlink

Just trying to figure out something that looks nice:

Contract Underaged => String { Alkohol } # Not sure if the block will be passed to String or Contract, though.
ContractWithSelf Alkohol, Underaged => String
SelfContract Alkohol; Contract Underaged => String

vlad-shatskyi avatar Jul 29 '15 22:07 vlad-shatskyi

Is this one possible?

Contract Alkohol => Underaged => String

vlad-shatskyi avatar Jul 30 '15 06:07 vlad-shatskyi

It is if you wrap everything in parenthesis and use .>> instead of => ;-)

But on the other issue we agreed that we don't want to expose Ruby people to currying concept here.. On Jul 30, 2015 8:38 AM, "Volodymyr Shatsky" [email protected] wrote:

Is this one possible?

Contract Alkohol => Underaged => String

— Reply to this email directly or view it on GitHub https://github.com/egonSchiele/contracts.ruby/issues/191#issuecomment-126202370 .

alex-fedorov avatar Jul 30 '15 08:07 alex-fedorov

Ok. I'm definitely up for Contract(Underaged).with Drink => String

waterlink avatar Sep 04 '15 22:09 waterlink

It seems we're coming up with the same kind of syntax suggestions for 3 problems, this one plus:

Namespacing: https://github.com/egonSchiele/contracts.ruby/issues/157#issuecomment-111726998

Contract {[ num, maybe[num] => String ]}

Arguments: https://github.com/egonSchiele/contracts.ruby/issues/113#issuecomment-87329450

Contract(:a, :b) { [ArrayOf[a], Func[a => b] => ArrayOf[b]] }

The three issues should probably be thought of together to prevent clashes and enhance simplicity.

I wonder if the arguments idea could solve this too somehow.

Underaged = lambda { |person| person.age < 18 }
# Have magic argument symbol :this that is always self
Contract(:this) { [Self[Underaged[this]], Drink => String] }
# But seems redundant as Self contract is needed to identify 
# the first in list as self contract rather than param contract anyway
Contract { [Self[Underaged], Drink => String] }
# Or make Contract able to take either: 
# a symbol for named args behaviour, lambda for self validation:  
Contract(Underaged) { [Drink => String] }
# You can use both then:
Contract(OldEnough, :drink) { [Alcohol[:drink] => Vomit[:drink]] }
# It makes sense to limit to only 1 self contract
# and any number of argument contracts.
# So maybe only the first can be a self contract as above,
# or use array for argument contracts:
Contract(OldEnough, [:drink, :bill]) { [Tab[:bill], Alcohol[:drink] => [Overdraft[:bill], Vomit[:drink]]] }
# Perhaps the array style could be optional

Or I may have completely missed what is going on, these things are a bit mind bending.

sfcgeorge avatar Sep 05 '15 00:09 sfcgeorge

I think you are correct.

Though I wanted to see this happening in more incremental way. (of course bearing in mind that all 3 should be implemented eventually and not making silly mistakes that will prevent from fixing other 2).

waterlink avatar Sep 05 '15 00:09 waterlink

I keep forgetting about invariants but it just occurred to me that they use a block syntax already, so my vote is on the bracebox syntax for any fancy functional features added.

ruby``` invariant(:day) { 1 <= day && day <= 31 }

sfcgeorge avatar Sep 07 '15 11:09 sfcgeorge