Today I learned about the F# and
operator. tl;dr: it is used for co-recursivity.
So on internet, someone was looking to flatten nested tuples in F#. Inspired by others Q&A, I answered it. I mixed a bit of reflection in a recursion function to make it work:
let isTuple tuple =
tuple.GetType() |> Reflection.FSharpType.IsTuple
let getFields (tuple: obj) =
tuple |> Reflection.FSharpValue.GetTupleFields |> Array.toList
let rec flatten fields =
fields
|> List.collect(
fun tuple ->
if isTuple tuple
then flatten (getFields tuple)
else [tuple]
)
let namer(tuple: obj) =
if isTuple tuple
then tuple |> getFields |> flatten
else [tuple]
namer (1, "test") |> printfn "%A"
namer (1, ("2", (3, "chicken"))) |> printfn "%A"
I don't remember if it hits me when I wrote it but today it did. We are using namer
in flatten
's collect
. Let's change that.
let rec flatten fields =
fields
|> List.collect(
namer
)
Alright now we face an error FS0039: The value or constructor 'namer' is not defined.
. This error is a feature. In programming, it is a good practice to order your functions to be read like a book (i.e from top to bottom). If a function f
calls a function g
, then g
should be declared before f
. Most popular languages dont care. Neither JavaScript nor C# at least.
In F#, this good practice is indeed mandatory.
The reason is that when the compiler sees namer
in the body, it doesn’t know about the function because it hasn’t finished compiling it yet! Top to bottom is mandatory. So what are we going to do in our case? We can't put namer
above flatten
nor we can do the other way around. We want them at the same level. So, how to have two methods calling each other? let rec... and...
is the syntax we seek.
let rec f() =
g()
and g() =
f()
The new version is clearly DRY-er:
let isTuple tuple =
tuple.GetType() |> Reflection.FSharpType.IsTuple
let getFields (tuple: obj) =
tuple |> Reflection.FSharpValue.GetTupleFields |> Array.toList
let rec flatten fields =
List.collect namer fields
and namer (tuple: obj) =
if isTuple tuple
then tuple |> getFields |> flatten
else [tuple]
namer (1, "test") |> printfn "%A"
namer (1, ("2", (3, "chicken"))) |> printfn "%A"
You can learn more about it over here. And this is how I learned about the and
operator.