TL;DR
I’ve been discussing Boolean Blindness with someone recently and I wondered if there would be a non-cumbersome way to achieve that with Elm’s type system in the vein of “making impossible states unrepresentable”. This would make Elm probably the first and only language that wouldn’t provide a Boolean type to users, don’t take my word for that, though .
Disclaimer
Note that this turned out to be a lengthy one and I’m not a computer language scientist and that this may be a solved and trivial problem I just didn’t have the grace to notice and I’m making a total fool of myself . I’ve certainly never seen something like this to this extent in a programming language yet but maybe I never noticed because the makers didn’t know or care to take out an admittedly crucial part of a language like Booleans…
Words, Words, Words…
As I understand it, in a nutshell Boolean Blindness refers to the fact that you’re squashing the actual meaning of a proposition down to one bit (i.e. True
or False
) and later you need to recover the meaning by keeping track of what the value actually represents inside your head using naming/convention/whatever. The type system can only help you in that it makes sure you’re comparing Booleans and naming is notoriously error-prone.
I’m sure most programmers were in a bug situations like this, which spawned the idea for this post:
canSubmit inputText =
-- (1)
String.length inputText > 0
update msg ({ inputText } as model) =
case msg of
ChangeText text ->
( { model | inputText = text }, Cmd.none )
Submit ->
-- (2)
if canSubmit inputText then
( { model | inputText = "" }, submit inputText )
else
( model, Cmd.none )
view { inputText } =
Html.div []
[ Html.input
[ Events.onInput ChangeText
, Attr.value inputText
, Attr.placeholder "Tell us something..."
]
[]
, Html.button
[ Attr.disabled (canSubmit inputText) -- (3)
]
[ Html.text "Submit"
]
]
Notice that the canSubmit
proposition is mapping down to True
or False
in (1)
, from here on the typechecker can’t help you with the semantics of the program. While the compiler happily confirms the soundness of your types there is a bug at (3)
, probably because it was copied from the update
function in (2)
. Granted, you could see the Attr.disabled
as the culprit with its horrendous API decision by W3C and well, errors happen, right?
I’m aware that you can always try to use union types in Elm to kind of avoid the Booleans you’re creating yourself but Booleans are all over the language and the libraries as well.
Another attempt would be to contain the squashing with the union type approach outlined above:
canSubmit : String -> SubmitCapability
canSubmit inputText =
-- (a) Be careful, we might need that information later
if String.length inputText > 0 then
OkToSubmit
else
NopeSureDont
update msg ({ inputText } as model) =
case msg of
ChangeText text ->
( { model | inputText = text }, Cmd.none )
Submit ->
-- (b) Not so bad but somewhat verbose
case canSubmit inputText of
OkToSubmit ->
( { model | inputText = "" }, submit inputText )
NopeSureDont ->
( model, Cmd.none )
view { inputText } =
Html.div []
[ Html.input
[ Events.onInput ChangeText
, Attr.value inputText
, Attr.placeholder "Tell us something..."
]
[]
, Html.button
[ Attr.disabled (canSubmit inputText == OkToSubmit) -- (c) Still a type-checking bug
]
[ Html.text "Submit"
]
]
In the canSubmit
function in (a)
we capture the actual meaning with a union type, with all the power it offers. In (b)
we now prefer to use a case ... of
instead of vanilla if ... then ... else
, note that this is still an option, the programmer doesn’t have to do this. Finally when using the information to determine whether the button should be enabled or not in (c)
reading the code makes it more obvious - at least to me - that this is probably not what you want but the type system would be as happy with this as with having the bug fixed via canSubmit inputText == NopeSureDont
.
Contemplation
I wonder if there would be a way to remove the explicit Bool
type and alter the familiar language constructs in favor of using union types or something of the sort to rule out these kind of errors. I don’t have hard numbers on how common these errors are but they are usually a notable source of errors in my code. The approaches I can come up with are the following - in the order I’m coming up with them right now:
-
i) You could have some annotation on a union type constructor that denotes a sort of
truthiness
, kind of like you have in JavaScript - only with, you know, type safety so the compiler would know how to interpret that constructor in a formerly Boolean situation-- Hypothetic Elm extension v1 type SubmitCapability = OkToSubmit as Truthy | NopeSureDont -- | HmmWhatDoWeDoWithMore -- Con: unclear what to do with N-value logic -- (b) Pro: preserves easy usage without actually dropping the meaning on the type level if canSubmit inputText then ... -- (c) Con: it is still possible to have the bug in here Attr.disabled (canSubmit inputText)
-
ii) Seeing that the more-than-two-constructors-problem could be alleviated by introducing syntax that constrains what you can do with the poorly dubbed hypothetic
type capability
which would make the mentioned con go away but would limit users, that would otherwise just use a normal union type by convention-- Hypothetic Elm extension v2 type capability SubmitCapability = OkToSubmit as Truthy | NopeSureDont as Falsy -- Pro?/Con?: Only two states allowed, -- which is as powerful as Bools but crippling -- for a union type -- (b) Pro: still nice if canSubmit inputText then ... -- (c) Con: same old, same old, still a type-checking bug Attr.disabled (canSubmit inputText)
-
iii) Personally I think crippling union types for this would not be worth the effort. We could introduce some kind of notion of
Positivity
that you would associate with a constructor of a union type and which would be recognized by the compiler. This sounds like putting a constructor into a category to me but my Haskell /Skala/… skills are far and in between-- Hypothetic Elm extension v3 type SubmitCapability = OkToSubmit as Positive | NopeSureDont as Negative -- Pro: all the power of union types, let's go haywire! -- This might be overkill, but it's fuuuuun | NotYet Time as Neutral | IDontKnow UrlToWebservice as Inconclusive | ThereAThreeHeadedMonkey as Evasive -- Con: there are certainly easy cases where a single -- constructor could be parameterized to be `Positive` -- and negative at the same time -- (b) Pro: still nice but tricky at the same time if we -- were to allow more buckets than black and white, -- is `Evasive` a yes or no? Maybe `Positive` is the -- only *truthy* category, I don't know if canSubmit inputText then ... -- (c) Con: hello darkness my old friend... Attr.disabled (canSubmit inputText)
-
iv) Which brings me to my next idea: what if the APIs we’re using would understand that notion of
Positivity
instead of relying on Booleans? Again, which might just be a typeclass/implicit/whatever. Obviously we can’t changeAttr.disabled
since that is in the HTML standard but a library author could very well annotate that function to react appropriately to theNegative
category by putting thedisabled="disabled"
we want in there, when the answer isNopeSureDont
. Provided you have annotated your union type correctly this would let you no choice but do the right thing! Talking about the pit of success situation: my guess is that this approach could possibly be applied to language constructs likeif ... then ... else
or library functions likeList.filter
, too, if we figured the bit about context and meanings ofPositivity
in that out…-- Hypothetic Elm extension v4 type SubmitCapability = OkToSubmit as Positive | NopeSureDont as Negative -- Maybe it's not such a bad idea to have -- the two-constructor-constraint? This could -- be loosened after the fact, though -- (a) Pro?/Con?: although never in question before, `(>)` would now act in -- terms of `Positive/Negative` - which could lead to massive confusion, -- maybe you regard a zero length string as `Positive`? What does that even -- mean? This sounds like the approach needs a way to put `Positive/Negative` -- in context, at least for builtin constructs? if String.length inputText > 0 then ... -- (b) Pro: we don't even have to change the code, it just does what you'd expect if canSubmit inputText then ... -- (c) Huge pro: you have no way to produce the flimsy bug, other than -- declaration and/or possibly context mismatch errors on the union type. -- The library would automagically know about what you consider `Positive` -- for your union type and do the right thing, which in this context means -- `disabled="disabled"`, if our return value is `NopeSureDont` Attr.disabled (canSubmit inputText)
The define-your-own-context-bit sounds intriguing but also way too complicated for replacing something silly like Booleans, I guess. Any thoughts on the matter? I’d be really curious if I’m the only one thinking about this sort of stuff. In my view it would be nice to have the type system catch even more stupid mistakes of mine . It’d be neat to have a discussion about this.