Card functional abstraction


#1

Originally published at: https://purelyfunctional.tv/modeling-solitaire-in-clojure/3-card-functional-abstraction/
This lesson teaches how to use functional abstraction to make changes to data structures quick and easy. This lesson includes a video screencast, a git repo, and two exercises. The video is 14 minutes long.


#2

For the first exercise, what exactly is meant by the “same” interface? The strictest definition would be for make, suit, and value to accept and return the same values as before; I doubt that is the intent, because the advantage of representing card values as integers would be lost.

A weaker definition would be for the functions to accept the same values, but allow their return values to change. This would result in (make :clubs (value (make :clubs :K))) being invalid (which might be OK).

If the value parameter for the make function becomes an integer, then (make :clubs (value (make :clubs 12))) is valid.


#3

Hi @cyberneticist,

What I mean by “same interface” is that you can call the same functions and expect the same behavior, regardless of how the card is represented in code. By making the function the interface, it doesn’t matter if they’re represented as maps or vectors or whatever.

Rock on!
Eric


#4

Hi @ericnormand,

I’m still unclear about which invariants need to be satisfied in order for the behaviour to be considered the same. The make function returns a card. If the card’s value is represented as a keyword, then (make :clubs :K) will return [:clubs :K]. If an integer representation is used, then (make :clubs :K) will return [:clubs 13]. The representation is exposed in a way that it typically isn’t in OO, but that’s OK so long as it is operated on via the functional abstractions.

But should (value [:clubs 13]) return 13 or :K? Are the values returned by the value function part of the behaviour we need to preserve? If, for this example, it must continue to return :K, then the advantage of the integer representation is lost (keywords are converted to integers, only to be converted back again immediately – though I suppose numeric-value could still exploit the integer representation if it ceases to use the value function). Alternatively, if it returns 13, then (make :clubs (value (make :clubs :K))) is invalid.

Or does the preservation of behaviour not extend to the parameters of the “constructor”, the make function? In that case, the constructor call would become (make :clubs 13), we get the benefits of the integer representation, and (make :clubs (value (make :clubs :13))) would be valid.

To put it another way: The values returned by make change to reflect the internal representation of a card, but that isn’t considered a change in behaviour because “card” is treated as a black box. Is the card’s value returned by the value function also considered a black box whose internal representation may change (from a keyword to an integer)?

Am I missing something painfully obvious, and making this more complicated than it needs to be?


#5

Hi @cyberneticist,

I’m just trying to add a thin level of indirection (the functions) over what is actually a concrete representation. What I consider the interface is simply:

(value card) returns a keyword. (numeric-value card) returns a number (for sorting). (make suit value) takes two keywords. These functions should work regardless of the data structure used to represent the card. This interface becomes the invariant, and I can now change the representation to suit different needs. It is undefined behavior to ask for the value of a card you make yourself without using the constructor.

make is guaranteed to produce something that can be passed to value and numeric-value. It may be a vector, or a map, or whatever.

But, notice, this indirection is only for convenience. We don’t have to use it, as you noted, since we already have an interface for vectors, maps, etc. The reason to use it is simply to allow for change, not “hiding” as you find in OO languages. It also gives a convenient place to do dynamic checks on the values (make sure it’s a valid value keyword, etc).

I don’t think you’re “overthinking”. I think it’s valuable to think through all of these possibilities. In the end, Clojure simply gives you tools to do what you want/need. I was using functions (a tool) as an indirection mechanism whose internals can change (change the code of the function) without changing the semantics (what it’s used for).

Rock on!
Eric