broquaint.com

by

Mal content is spent

So far on this learning journey through TypeScript I've started a Dungeon Crawl Stone Soup game wins follower (part 1, part 2) and implemented the first 4 steps of Mal (Step 0: the REPL & Step 1: Read and Print, Step 2: Eval, Step 3: Environments). Logically next is more Mal and in this case I'm tackling two steps: Step 4: If Fn Do and Step 5: Tail call optimization.

Two steps forward

The reason I'm covering two steps in this post as that there's not much to report in terms of what was learnt. This is quite typical for learning a language when implementing Mal—a lot of the learning happens on the first few steps, as you familiarize yourself with its concepts and facilities, then when you're comfortable with the language you can focus on applying that learning to the remaining steps.

Almost control flow analysis

So in Step 4 one thing that needs implementing is an equality check (i.e (= foo bar) test if foo and bar are equivalent1), which is always an interesting part of the implementation as you are dealing with both a heterogeneous set of Mal types and also reconciling its comparison requirements with how the host language works.

A straightforward way of doing this in a lot of languages is to match/switch on the Mal type and then compare when the two values being compared share a type (otherwise they are necessarily not equivalent (with an exception for lists & vectors)). I had hoped it would be as simple a something like this:

if(a.type === b.type) {
    switch(a.type) {
        case 'number':
            return a.value === b.value
        // …
    }
}

Which logically speaking should hold—we've asserted that a and b have the same type property so are the same type2 and therefore both will have a value property. However—as the red squiggly indicates, TypeScript doesn't know what to make of b.value:

Property 'value' does not exist on type 'MalType'. Property 'value' does not exist on type 'MalList'.

So the type checker has enough context in the case branch to know that a.value is fine but the control flow analysis from the earlier if statement doesn't propagate and hold that b.value must also necessarily be fine. The fix was, of course, to just do the comparison in the case branch (but it's a bit clunky with multiple types).

I presume it just isn't supported, rather than being impossible, because it's either too uncommon a use case or just complicates the control flow analysis enough not to warrant the effort. At any rate it was a learning experience!

It does the job

The only other thing of note was finding a use for a type predicate to simplify working with sequence–like types:

function isMalSeq(v: MalType): v is MalSeq {
    return v.type === 'list' || v.type === 'vector'
}

That predicate can then make code like this:

'empty?': mal.function((v: MalType) => {
      return mal.bool(v.type === 'list' || v.type === 'vector' ? v.values.length === 0 : false)
      }),
Into code like this:
'empty?': mal.function((v: MalType) => {
    return mal.bool(isMalSeq(v) ? v.values.length === 0 : false)
}),

Which is simpler and the intent clearer.

While this is handy it feels awkward as a means of expressing a type predicate but I can appreciate that, once again, TypeScript is aiming to produce as much unadulterated JavaScript as possible and this feature (extending function syntax a little) allows for just that. Pragmatic solutions aren't always beautiful solutions but you can't argue with the results (I mean you can, but maybe don't do that).

Returns diminished

I didn't cover much of the actual Mal implementation as there wasn't much to cover (as it stands it is now Turing complete and has tail–call optimization!). Which brings me on to the conclusion of this series …

Looking ahead to upcoming Mal process steps 6 through A I can see that the implementation will continue to be an enjoyable exercise but I doubt it will require learning a great deal more about TypeScript. So, for this blog at least, I'll be ending my fun with Mal in TypeScript and switch back to other thing wherein I'm more likely to work with more libraries, type definitions and other facets of TypeScript.

Footnotes

1 Further reading: Equal rights for functional objects or, the more things change, the more they are the same by Henry Baker and Clojure's equality guide for what that looks like in practice.

2 Don't be like me and use the word "type" in anything that relates to types, it's a bad idea—you will reach semantic satiation within moments of beginning any discussion on the matter. I think I can get away with it here because this isn't code that others will ever have to work with.