Sun, Mar 7, 2010, 5:16pm F-Script Switching Options
Programming » F-Script
(Last updated: Wed, Mar 10, 2010, 12:26am)
T
here are many ways to think through the flow of a program, and times when certain constructs like switch statements are a nice option, even in a language with no syntactical support for it, per se. Philippe Mougin was discussing just such options in F-Script back in the Oughts. I had a need for switching in F-Script, and came up with a few versions that I found useful.

First, let me point out the very graceful construct mentioned by Philippe in his blog. As traditional switch-case logic is about starting with a dictionary of key/block pairs and using some variable to select among them, F-Script already can handle this association gracefully:

( fscript )
  1 "Method 0"
2
3 (#{
4 possibleValue1 -> [ action1 ],
5 possibleValue2 -> [ action2 ],
6 possibleValue3 -> [ action3 ] "etc"
7 } objectForKey:myVariable) value.

(Philippe breaks his up into two statements for clarity. Either way seems nice.) Here, if a value is a perfect match for myVariable, the code block associated is returned by the objectForKey: method and e-value-ated. Graceful, with only the slightly busy-looking starting (#{ syntax.

It lacks two things from switch logic in other languages: the else/default case; and the ability to "fall-over," for either covering multiple cases or for cascading effects. I very rarely like to use the latter feature (logic intent is often hard to make obvious), but I would like to have an else-case default block run when all else doesn't match.

In this first variation, I make a block called "switch" that first evaluates the selected code blocks exactly as above, but also returns a single YES/NO value, made up of or-ing all the YES/NO responses to check of the value against any key. So, if any key matches, the switch block returns YES; if all are misses, it returns NO, which you can then use to optionally trigger an else-case code block:

( fscript )
  1 "Method 1"
2
3 switch := [ :var :dict |
4 (dict objectForKey:var) value.
5 dict allKeys = var \#|.
6 ].
7
8 (switch value:myVariable value:#{
9 possibleValue1 -> [ action1 ],
10 possibleValue2 -> [ action2 ],
11 possibleValue3 -> [ action3 ] "etc"
12 })
13 ifFalse: [ elseCase ].

Ever so slightly more complicated syntax gains an else case, includes the word "switch" to perhaps make the intent slightly clearer, and pops the variable which triggers the cases on top.

The next variation has the advantage of returning the results of the evaluated block, in case you'd like to use it for something instead of just executing blocks.

( fscript )
  1 "Method 2"
2
3 switch := [ :var :dict :else |
4 (dict allKeys = var \#|)
5 ifTrue:[ (dict objectForKey:var) value ]
6 ifFalse:[ else value ].
7 ].
8
9 x := switch value:myVariable value:#{
10 possibleValue1 -> [ action1 ],
11 possibleValue2 -> [ action2 ],
12 possibleValue3 -> [ action3 ] "etc"
13 } value: [ elseCase ].

Note that Method 2 takes in 3 arguments, taking away the need to wrap the switch block in parentheses, but swapping the more readable ifFalse: for value: at the end. Of course, using the result is entirely optional, but now not including an else block is not optional.

Lastly, (the first variation that I made and my favorite) is something not technically a switch-case logic, but can be used for it. It's a generalization that allows completely arbitrary sets of conditions as keys for each block of triggerable code. Here one does not even need to think in terms of a variable which selects among the conditions, though one can decide to use it that way. It has several advantages, but since the logic behind it is different enough, I call the block "conditional" instead of "switch" as above:

( fscript )
  1 "Method 3"
2
3 conditional := [ :dict | | keys |
4 keys := dict allKeys.
5 ((dict objectForKey:@keys) at:(keys value)) value.
6 keys value \#|.
7 ].
8
9 (conditional value:#{
10 [ condition1 ] -> [ action1 ],
11 [ condition2 ] -> [ action2 ],
12 [ condition3 ] -> [ action3 ] "etc"
13 }) ifFalse: [ elseCase ].

The conditional block itself only takes one dictionary for its argument, and again makes available the optional ifFalse: logic at the bottom because it returns the or-ed condition results as with Method 1.

How this works is by wrapping each condition1, condition2, etc, in blocks. This makes each one unique, even if their evaluation is the same*. This gives a great deal of flexibility. One can use it for switch-like logic, but one can also cover many kinds of cases, related or not. It is perhaps too flexible for some, as some crazy contortions can become tempting. But if you try to keep tight-related conceptual operations per such logic dictionary, it can enhance little moments in your code. Yes, it's just a graceful if-then set, but isn't all of this, and it is graceful.

(* — It is a touch dangerous because even two blocks with the exact same condition are not the same block, so the code relies on your oversight to not accidently put in multiple responses to the same condition. Sorting the dictionary lines can make most bugs along these lines easy to find.)

Thoughts? More ideas?

  • Philippe Mougin (Tue, March 9th, 2010, 12:45pm UTC)
    Nice! I think it can even be enhanced a little bit by using out of dictionary associations (a little known feature of F-Script 2.0). Actually, -> is a regular method that can be used to associate any two objects (then known as "key" and "value"). Such an association is represented by an FSAssociation instance, and is not limited to dictionaries creation. For example, we can have arrays of associations, which we can use for the construct presented here. This gives something like:
    ( fscript )
      1 conditional := [:array|
    2 (array value at:array key value) value.
    3 array key value \#|
    4 ].
    5
    6 (conditional value:{
    7 [ condition1 ] -> [ action1 ],
    8 [ condition2 ] -> [ action2 ],
    9 [ condition3 ] -> [ action3 ]
    10 }) ifFalse: [ elseCase ]

  • Jeff (Tue, March 9th, 2010, 2:14pm UTC)
    Ah! I hadn't realized how -> was being used. Excellent. So this also effectively makes avalaible ordered dictionaries. I like your term dictionary associations, which can make for some interesting options, like multiple identical keys:
    ( fscript )
      1 F-Script> d := #{ 'this' -> 'that', 'this' -> 'other' }
    2 F-Script> d objectForKey:'this'
    3 other
    4 F-Script> d
    5 {
    6 this = other;
    7 }
    8 F-Script> c := { 'this' -> 'that', 'this' -> 'other' }
    9 F-Script> (c at:c key = 'this') value
    10 {'that', 'other'}


    Also "value" is a keyword for two different kinds of methods (key/value and e-value-ations), which could lead to some head-scratchers if one was playing hard and fast with their data structures. Just posting the first pathological cases that comes to mind:
    ( fscript )
      1 F-Script> e := { 'this' -> { 'that' -> 'other' } }
    2 F-Script> e value value
    3 {{'other'}}
    4 F-Script> e := { 'this' -> { 'that' -> [ 'other' ] } }
    5 F-Script> e value value value
    6 {{'other'}}
    7 F-Script> e := { 'this' -> [ { 'that' -> [ 'other' ] } ] }
    8 F-Script> e value value value value
    9 {{'other'}}

Leave a comment