Another episode in my series on making a bridge program. Earlier I wrote about the fuzz of modeling 1 SA (a game without trump) because it requires the introduction of a new suit that does not really exist. Turns out bidding 1 SA also poses some challenges.
As you can imagine, when you play without a trump, it is important to have some cards in all suits. Imagine both you and your partner are low on clubs. When the opponent starts playing them, there is no way that you get the trick back, as there is no trump.
Hence, ACOL prescribes that you only bid 1 SA if your hand is “balanced”: which is typically defined as having at least 2 cards in every color.
That will require lots of ands to express in the model we are currently using:
(both (points 15 17) (both (cards 2 13 Clubs) (both (cards 2 13 Diamonds) (both (cards 2 13 Hearts) (cards 2 13 Spades))))
My first intuition was to add a bigger ‘both’, called all:
let all (p:Condition)(q: Condition)(r:Condition):Condition = And(And(p,q),r)
That will allow us to combine three conditions and write:
(both (points 15 17) both (all (cards 2 13 Clubs) (cards 2 13 Diamonds) (cards 2 13 Hearts)) (cards 2 13 Spades))))
Meh, not really nice still. Of course we could add one more argument and make the and over 4 conditions, but the result would still be long, and you miss the obvious point that it holds for all suits:
(both (points 15 17) (all (cards 2 13 Clubs) (cards 2 13 Diamonds) (cards 2 13 Hearts) (cards 2 13 Spades))
There are benefits too, it is nice that this all is generic, so we can combine all sorts of conditions (not just: for all suits), but in the end I gave in and introduced ‘universal’ quantification over all suits. Less generic, but quite compact:
let forAllSuits (p:Suit -> Condition): Condition = both (both (p Clubs) (p Diamonds)) (both (p Spades) (p Hearts))
I say ‘universal’ because it obviously is not universal, as it can only quantify over all (real) suits. A step closer to Turing completeness 🙂 My tagline about DSLs is “before you know it, you are Turing complete”*
But, all jokes aside, how to use this new quantification?
We can use forAllSuits in this way:
(both (points 15 17) (forAllSuits (cards 2 13))
Let’s inspect what is going on here, because if you are not too familiar with functional programming, this might be a bit freaky. Function forAllSuits takes as input a function of type p:Suit -> Condition. That means the argument function takes a Suit and returns a Condition. Based on that input, forAllSuits returns a Condition.
So far nothing all too strange, we could call forAllSuits with a lambda, for example like this:
forAllSuits (fun s -> cards 2 13 s)
Remember the definition of cards is:
let cards (min:int) (max:int) (s:Suit) = NCards (min, max,s)
The lambda takes a suit and returns the condition that there should be between 2 and 13 of that suit. forAllSuits with that argument creates a conjunction of this for all 4 suits.
But, that is not how we call it, we do something more compact, we use ‘half a function’ as argument, namely:
forAllSuits (cards 2 13)
Cards is a function (see above) that needs as input two integers, and a suit. In F# and other functional languages, it is allowed to submit fewer arguments than needed, which will result in a function that has the same output type, and has the supplied arguments fixed. This is called partial application.
A supersimple example is a function that adds a fixed value, like this:
let addFive = (+) 6 // partial application add4Five 1 //this is 6
(cards 2 13)
This now still needs a Suit, but as such it fits perfectly in forAllSuits, because it needs something that takes a suit and returns a function.
forAllSuits (cards 2 13)
Reads like a book, right?? What do you think? This is work in progress, so let me know 🙂
* which in Dutch is better because it rhymes: “voor je het weet, ben je Turing compleet”