Explicit Is Better Than Implicit? I Disagree!
On how to smooth your API for call site usage and make your API-Users happier.
Typescript Sweetness
Typescript has a really nice feature where you can define a discriminate union like type with very little syntactic overhead
function foo(x: String|Boolean){}
(Wasn’t that supposed to be a blog about F#? Hold on young padawan. We’re getting there!)
The usage of foo
is pretty easy
foo("bar");
// or
foo(true);
//but the line below will not compile
foo(1);
To do something usefull with the param x
within foo
there needs to be code within foo
that descriminates between the
types. A sort of pattern matching. Usually this looks similar to this
function foo(x: String|Bool){
if (typeof(x) == "String") {
someStringFun(x)
}
else {
someNumberBool(x)
}
}
foo("bar");
The F# Way
In F# there is this notion that explicit is better than implicit. So in order to have something remotely similar in F# you have to do quite a bit of heavy lifting
AND your call site will be wordy beyond belief.
We start with a explicit discriminate union type as function parameters in F# need to be of exactly one type.
type FooParam =
| BoolParam of bool
| StrParam of string
Then we write the function that we want to have a pattern match like the Typescript example
let foo (x: FooParam) =
match x with
| BoolParam y -> printfn "Bool: %A" y
| StrParam z -> printfn "String: %A" z```
On the call site we then create an instance of the newly wrapper DU and feed it into the function.
foo (BoolParam true) |> ignore
Essentially this works. However we have forced our function user to KNOW about that wrapper and he or she needs to create instances for it.
So far so good (or better ugly). Aren’t there better ways? Well in Haskell …
Haskell Anybody?
(“Oh no, oh no - not Haskell again” screams the young padawan in agony. To which I say sit still and learn!)
… in Haskell you have type classes for this.
class Foo a where
foo :: a -> a
A Haskell class is nothing like an OO class rather something like an interface in Java only even more abstract. You implement that interface by creating an instance of it by supplying concrete types.
instance Foo Bool where
foo x = x --useless impl but you get the idea
instance Foo String where
foo x = x
The cool thing about that is that every implementation for a specific type is completely detached from any other implementation for another type. And both implementations could be in totally different modules. Kinda like .NET extension methods. And then you just call the function with the parameter value of the needed type. No Wrapping no knowing about wrappers.
x = foo True
y = foo "Bar"
Back To F# And Operator Overloading
So is there a way we can easen the pain of doing this in F#? Yes indeed vote for this language suggestion on type classes
and while you do I show you how to get something similar in F#.
First let me introduce you to .NETs implicit conversion operator. “Implicit conversion operator” you ask? So it is possible after all?
Let’s see. The signature of that rare creature is static member op_Implicit(x: 'a) : 'b
. Let’s see what we can do with that.
type FooParam =
| BoolParam of bool
| StrParam of string
with
static member op_Implicit(x: bool) = BoolParam x
static member op_Implicit(x: string) = StrParam x
After we have defined those overloaded methods we call our foo
function without the wrapper.
let bar = foo true
Sadly no implicit conversion here - just an compiler error that foo expects a FooParam not a bool
. So we still have to somehow force that conversion (aka make it explicit).
For that to do we first create an inline function that calls that conversion function
let inline (!>) (x:^a) : ^b = ((^a or ^b) : (static member op_Implicit : ^a -> ^b) x)
and use that newly created operator explicitly.
let bar = foo (!> true)
That does work indeed now. A little bit too many parens for a non-lisp source code file. But OK.
Wait! If we can use the operator !>
explicitly why not move the operator into our function itself? Let’s try that and also mark the function as inline.
let inline foo x =
let z = !> x
match z with
| BoolParam y -> printfn "Bool: %A" y
| StrParam z -> printfn "String: %A" z
Now that looks promising: the inferred type is val foo : x:'a -> unit (requires member op_Implicit)
.
Calling foo
yields the effects we expect without any errors.
let bar1 = foo "bar"
let bar2 = foo true
Finishing touches
While we were massaging our type system we lost one ability however: foo
can’t handle FooParams
anymore.
let bar1 = foo (StrParam "bar") //compile error now
This is easily solveable: we simply need to add one more op_Implicit
member to our type
type FooParam =
| BoolParam of bool
| StrParam of string
with
static member op_Implicit(x: bool) = BoolParam x
static member op_Implicit(x: string) = StrParam x
static member op_Implicit(x: FooParam) = x // <- trivial
Now calling foo
with a FooParam
works.
let bar1 = foo (StrParam "bar") //compile error now
So everything is OK then?
Unfortunately not! All of the above will work when we have full control over the definition of our synthetic parameter type. But what if we are faced with a type that we want to convert to that
isn’t under our control? Let’s assume there is a type ForeignModule.Result
which is defined as
module ForeignModule =
type Result<'a, 'b> =
| Success of 'a
| Error of 'b
We can try to extend that type within our own module via extension methods
module OurModule =
open ForeignModule
type Result<'a, 'b> with
static member op_Implicit(x: string) = Success x
However that wont work as operators can not be added via extension methods. sigh. For this to work we need to use a completely different approach. We hace to create a type purely for transformation purposes and some static methods in it.
type ToResult = ToResult
with
static member ($) (_: ToResult, l: string) : Result<string, string> = Success l
//and again a method that just returns the input so we can handle strings and Results
static member ($) (_: ToResult, l: Result<string, string>) : Result<string, string> = l
And than a helper method to trigger those static methods
let inline toResult x : Result<string, string> = ToResult $ x
Please dont ask me why we have to use the ($) here - It doesn’t work any other way.
Finally we write the function that uses our implicit conversion
let inline bar x =
let z = toResult x
match z with
| Success s -> sprintf "Success: %A" s
| Error e -> sprintf "Error: %A" e
Young padawan, our lecture is coming to an end. A few parting words still …
Limits
There is some limitation when overloading and using generic type constraints. For example overloading on the same generic type with different generic params like this
type ToResult = ToResult
with
static member ($) (_: ToResult, l: Result<string, string>) : Result<string, string> = ...
static member ($) (_: ToResult, l: Result<int, string>) : Result<string, string> = ...
wont work and give you a compiler error telling that no fitting overloaded method could be found. This will happen at the call site and might be confusing if you don’t know what is happening.
Explict vs Implicit
I think when talking about explicit vs implicit type conversions we should in rather talk about conversions that we have control over (in principle) as opposed to conversions we can’t control.
Javascript’s infamous "" + true
coming to mind.
So yes explicit is necessary however that should most likely not happen at the call site but thru other means.
Cast your votes
As you have seen - almost everything is possible already right now. However it takes quite a bit of heavy lifting and greets you with some strange errors sometimes. So please vote for type classes or if you can’t at least vote for erased union types
If you have any comments drop me a note on twitter or via email. You’ll find the contact info on my homepage