Skip to content

Latest commit

 

History

History
158 lines (112 loc) · 11.9 KB

File metadata and controls

158 lines (112 loc) · 11.9 KB

if considered harmful

use case/switch instead. in a nutshell, switch is declarative and if/cond is procedural.

it’s actually already being done: haskell’s if is syntactic sugar for case on True & False constructors of the Bool sum type. switch syntax should be terse; for example, `x

1: a; b;; 2: c;` for what in c would be switch(x){case 1: a; b; break; case 2: c;}. this is no longer than an if statement. in a tacit, concatenative language, it could be just 1:[ a b ] 0:[ c ] sw vs [ a b ] [ c ] if. the syntax could simply be that, like c enumeration syntax, if case numbers are omitted then natural numbers are assumed, starting from the last-declared case number (so the false branch would precede the true one). rather than store multiple booleans as vars by name or in the stack, mask them together then run a switch statement. this is flat and makes tracking all booleans explicit and thus easy. for example:

TODO: put in an example

using numbers afforded me to refactor from the oldest to newest versions because it reduced complexity both in that there were fewer variables to track, and simpler control structure i.e. mentally tracking which data is on the stack and in which order, or which booleans hold in a given conditional branch, and in factor, balancing if branches to have equal stack effect.[1] you still must ensure equal effect of case branches, but it’s much easier since both 1. case is flat, and 2. one input is tested against each case branch, whereas each cond branch’s effects depends on its predecessor’s branch’s effect.

one might erroneously think that the old version could be equivalently rewritten by combining into a single test case: getcharge 98 >= pluggedin? and then inlining it with the other if branches, all expressed as one cond, but that would be wrong because most of the clauses actually require that we are not plugged-in. if we incorporate the pluggedin? bool as a bit in a number, then we immediately know from the case number whether, in that case, we’re plugged in or not; in other words, it makes each case, independently, an effortless & elegant expression of all of which booleans are true and all of which are not, altogether/simultaneously.

btw, switch has better execution time than cond since it’s just a lookup in a table rather than multiple conditional branching.

other concerns which i didn’t notice in this case, but i’ve encountered before:

  • type safety won’t help me notice whether two booleans are accidentally swapped! that kind of error is much easier to make than incorrectly masking together booleans!

  • reordering case [switch] statements (assuming that none falls-through to the next) doesn’t change the program. reordering cond branches does, since each branch is with respect to its predecessors. also case is flat, so rearranging its source code is much easier than rearranging nested if clauses!

limitation of switch

consider 4 booleans a b c d of values t f t t, and their compound representation x:10112. if you want to test specific combinations of the 4, but also want to test combinations of only a proper subset, e.g. a|!d then you could enumerate up to all 32 cases, which, with fall-through and default might not be too bad, but you can see how this becomes problematic for 4 or more booleans. in this case, we could use cond wherein each or most branches have a switch on a subset of values e.g:

switch(x & 0b1001) { // select only a & d from x i.e. normalize b & c to 0
  case 0b1000 | !0b0001: // a | !d i.e. 0b1110
  ...
  y = 1;
  break;
}

if(!y) {
  switch(x & 0b0110) { // select only b & c from x
    case 0b010: ... // (!b)|c
  }
}
Tip
AND is the natural joining method; "it’s raining. it’s cold." is the same as "it’s raining and it’s cold." or (disjoint union) is a less-common case.

i advocate switch instead of cond because it’s flat: branch order is meaningless (except fall-throughs) and each case value contains all the information so you don’t have to look elsewhere nor track which conditions failed or succeeded by the time that you test the case. if you use cond such that order is irrelevant (i.e. each cond test clause contains all data used across all test cases, explicitly including whether each is true vs false) then it’s just as well to use cond. the switch technique certainly works well for 3 or fewer booleans, which is a common scenario.

fall-through

to map a set of values to a computation, omit break:

switch(x){
  case 1: case 3: case 5: case 6: case 8: ...; break;
  case 9: ...; break;
  case 11: ...; break;
  default: ...; break;
}

of course, if c had better syntax, then it’d just be:

switch(x){
  1 3 5 6 8: ...;;
  9: ...;;
  11: ...;;
  default: ...;;
}

the "switch" idiom can be effortlessly ergonomic, if only sensible syntax is chosen.


[1] for example, this code, which i’d intended to be ( ? n — str ) fails due to imbalanced `if’s:

{ { [ 2dup 7 eq? and ] [ 2drop "FULL" ] }
  { [ dupd 0 eq?     ] [ 2drop "SLEP" ] }
  { [                ] [ "LOW"        ] }
} cond

yet if i change the 2nd 2drop to drop, and move the other drop into the predicate of the 3rd test, then it succeeds:

{ { [ 2dup 7 eq? and ] [ 2drop "FULL" ] }
  { [ dupd 0 eq?     ] [ drop "SLEP"  ] }
  { [ drop           ] [ "LOW"        ] }
} cond

if you keep your wits about you, then this is obvious, but it’s easy to become awash in code and start becoming hazy and start making mistakes like this, confusing the stack effect for a branch/body clause with the stack effect of the test clause which leaves data on the stack for the next test.

switch is cool because it re-expresses many things as equality. for example, 5 > is now a boolean, a bit in a number, which we test for equality. if we have many such cases of greater/less than, then bins gives an integer corresponding to the range, which we can switch on.

TODO: find a more-complex version; this one is too simple to demonstrate.

suppose that i want to perform some operations on filepaths (two of them, as it were: a source & destination), but only if they exist and are directories. we must check that they exist first, else we get an exception. being sensible programmers who provide helpful, complete error messages, we want to say which of the paths didn’t exist, or if it did, whether it was not a directory.

the naive, inelegant way
if not os.path.exists(src):  exit(src  + " does not exist")
if not os.path.exists(dest): exit(dest + " does not exist")
if not os.path.isdir(src):   exit(src  + " is not a directory")
if not os.path.isdir(dest):  exit(dest + " is not a directory")

this exploits that exit short-circuits. it is verbose, repetitive, and uses booleans. sounds like cond. let’s use integers (switch-like). first, note that integers generalize booleans (ℤ/2). rather than two possible values, we have multiple. however, we still want to decide whether to exit or not; we must have a partition fn that returns 0 or 1 from any of the integer cases that we may produce.

{ src dst } [ file-exists? ] reject [ ] [ [ " does not exist" append print ] each 2 exit ] if-empty

it’s off to a good start: we tell about all troublesome paths, but yet insofar as "trouble" means "doesn’t exist." we want to add another condition (boolean), for which we could use the above scheme, but here we must check the second boolean only if not the first! this is the pattern of finding the earliest predicate that fails.

[ { [ file-exists? not ] [ directory? not ] } swap [ swap call ] curry find drop ] map

produces f, 0, or 1 for an extant directory, a non-existant file, or a extant non-directory. then we just need to filter-out the falses (since they’ve no error), then print the corresponding error:

[ dup { [ file-exists? not ] [ directory? not ] } swap [ swap call ] curry find drop 2array ] map
[ second ] filter [ first2 { "does not exist" "is not a directory" } nth " " glue ] each

and we’re all set. the not's & swap's uglify, but this is only because i’m using find counter to its usual design; i’m using find like cond. its expression in j is (almost) much more elegant simply because of argument ordering and that we can use either 1: or 0:, or i. or i:. sadly, we must pretend that j has higher-order fns, that `:0 takes m as a left arg.

,/@:>"_1(]#~0<#@>@{:"1)paths,.(' is not a directory';' does not exist';''){~fexists`isdir(`:0 i.0:)"0 _1 paths

which would be great except for the weird padding and, again, that we can’t just fill arbitrary arguments of adverbs or conjunctions, namely `:, here.

good, but how to express non-functionally? in c we can’t put predicates into an array; we must sequence them in code i.e. unroll find, which would be cond expanding to `if’s except storing the branch number. yet for this simple example, since the case number is only used to lookup a string, we can just use the string directly, and because there are only two predicates where one depends on the other, we must use one branch, but once we’ve done that, we already are finished, so i guess this is too simple an example to well demonstrate the general case:

dup [ dup file-exists? [ directory? f "is not a directory" ? ] [ drop "does not exist" ] if ] map
[ [ " " glue print ] [ drop ] if* ] 2each

(written in factor b/c the linux api is unusable.)

still, this code is cleaner & shorter than the functional variant!


TODO: merge this into "fns (and so also lambdas) considered harmful" and "higher-order fns suck". lambdas are just a delayed execution primitive. if & case are also such examples. should the user be able to define their own places of delayed execution? why not have a well-designed, particular delayed execution primitive built into the language, without the user being able to define their own (so like a better version of case)? again, consider apls, which use e.g. group or sort without higher-order fns. also, compare macros to higher-order fns. macros are really the exact same thing but which execute at parse time, right? this really begs the question of why anyone would ever want to use a macro instead of a higher-order fn. even in k i defined case as a higher-order fn. k has no macros.