DEV Community

loading...

First Impressions of F#

dewofyouryouth_43 profile image Jacob E. Shore ・3 min read

One of my goes for 2021 is to learn C# and F# (I mostly work in Python and JavaScript). More details about New Years resolutions here.

I've started to get my feet wet in F#. I've discovered .NET interactive notebooks in VS Code for C# and F# - which makes playing around with them more fun. (I wonder if there is a way to embed them in markdown, but I digress.) Now that I've had an opportunity kick the tires a bit, here are some of my discoveries:

There are some things that strike me as a little odd, but I'm sure they were intentional decisions.

Unlike most languages, in F# = is used for both assignment and equality. So you can do:

let my_bool = 10 = 2
let this_is_ten = 2
printf "%b" my_bool // false
printf "%i" this_is_ten // 2

Enter fullscreen mode Exit fullscreen mode

I suppose this is similar to SQL. Similarly, != is not used to show inequality but rather <>.

Also, in F# you don't have to declare the type. F# figures the type out for you. But apparently, this can happen even after you declare something. For example:

// This is a function that adds two things together
// but what kinda of things?
let add_stuff x y = x + y
// Here, the compiler decides that
// I'm concatenating strings with this function
let hello = add "Hello " "world!" //  "Hello world!"
// If I then try to add numbers I will get a type error
let my_sum = add 10 30 // Error: expected strings, got ints
Enter fullscreen mode Exit fullscreen mode

In an alternate reality - if I reversed the order, this would happen:

let add_stuff x y = x + y
let my_sum = add 10 30 // 40
let hello = add "Hello " "world!" // Error: expected ints, got strings
Enter fullscreen mode Exit fullscreen mode

So it seems like, even though the definition of the function is identical in both cases, it is further defined by how I first use it. I find this counter-intuitive and would rather just define my function from the get-go.

The Microsoft docs make a big deal out of not needing to define your types. I'm not sure why this is a feature. I'd rather be able to just look at a function and know how it behaves without seeing how it was used. Maybe there is something I'm missing here.

Of course, I don't have to let the compiler infer the type, I can be as explicit as you want to be. Here I'm telling my functions exactly what I want them to recieve.

let double_list (l: List<int>) = List.map(fun x -> x * 2) l
let add_one (l: List<int>) = List.map(fun x -> x + 1) l

// I can put them together like this:
let double_then_add = double_list >> add_one
// I can also do it like this:
let add_then_double = double_list << add_one
// Which is the same as this:
let also_add_then_double = add_one >> double_list

let new_list = double_then_add [1..10] // [3; 5; 7; 9; 11; 13; 15; 17; 19; 21]
let another_new_list = add_then_double [1..10] // [4; 6; 8; 10; 12; 14; 16; 18; 20; 22]
let a_third_new_list = also_add_then_double [1..10] // [4; 6; 8; 10; 12; 14; 16; 18; 20; 22]
Enter fullscreen mode Exit fullscreen mode

The >> operator is called the composition operator.
Theres also a piping operator like this:

[1..100]
|> List.filter(fun x -> (x%2) = 0) // filter out all odd numbers
|> List.map(fun x -> x * 2) // Multiply them all by 2
|> printfn "Even doubles: %A"

(**
    Will return this:

        Even doubles:
        [4; 8; 12; 16; 20; 24; 28; 32; 36; 40; 44; 48; 52; 56; 60; 64; 68; 72; 76; 80;
        84; 88; 92; 96; 100; 104; 108; 112; 116; 120; 124; 128; 132; 136; 140; 144; 148;
        152; 156; 160; 164; 168; 172; 176; 180; 184; 188; 192; 196; 200]
*)
Enter fullscreen mode Exit fullscreen mode

I'm not 100% yet on what the difference between piping and composing is.

If anyone else has insights on F# that they feel could be helpful - or would like to clarify things for me, it would be most appreciated. :)

Discussion (8)

pic
Editor guide
Collapse
drewknab profile image
Drew Knab • Edited

Addressing your add function first: this is part of the type inference system where F# does Automatic Generalization and Type Inference. The compiler just makes an assumption based on how the function is first used. This is actually very useful when you get into currying and partial application.

If you really want your function to be able to both add two numbers and concatenate strings, you have an option. You can write: let inline add_stuff x y = x + y and that would work. You can read more about Inline Functions, they're pretty useful but can have some downsides depending on how they're used.

I would like to add that just because you can or could do something doesn't necessarily mean you should: add two integers together and return the sum is logically distinct from concatenate two strings and return the combined string. It's (mostly) fine in this trivial example, but the compiler is trying to protect you from writing logically questionable code by yelling at you by default.

As for your question of composition operator vs pipe operator.

Uhhhhhhhhhh, well, they're mostly the same with a distinct difference. I'll refer you F# for Fun and Profit

Collapse
dewofyouryouth_43 profile image
Jacob E. Shore Author

Thanks for your help I'll check this out.

Just to clarify, I don't think it's a good idea to have a function that does both addition and concatenation. My reservation was just that it means you can't just look at the function and immediately know what it's going to do.

Collapse
drewknab profile image
Drew Knab

Sure thing.

Gotcha, sure. However, depending on your environment you can see what types the compiler is assigning to your functions. I predominately use Ionide in VS Code, but that extension is available for other text editors as well. Similarly, you get a type signature when you declare a function in fsi.

Collapse
asik profile image
André Slupik • Edited

Like you've correctly identified, >> is the composition operator. If you have two functions:

f: A -> B
g: B -> C
Enter fullscreen mode Exit fullscreen mode

Then f >> g is a new function from A to C. Concretely:

let h = f >> g
h(x) = g(f(x))
Enter fullscreen mode Exit fullscreen mode

The pipe operator is just a way to reverse function invocation. Concretely:

f(x) = x |> f
Enter fullscreen mode Exit fullscreen mode

It allows you to write the argument before the function. You'll find this useful in many situations.
In general, the pipe operator is used everywhere (that's where F# gets its logo!); the composition operator sees more limited use. See docs.microsoft.com/en-us/dotnet/fs... - implicit arguments don't play well with tooling, so they are a tool to use sparingly.

Collapse
onpikono profile image
Ondrej Pinka

As for the functions / values without type signatures, an IDE (VS Code + Ionide extension, or Visual Studio) can display them for you.

It is interesting that something you find confusing (how compiler infers function's type based on which "variant" is used first) I find really clear and actually very helpful. I am a total beginner but I already feel like I am in control of my own code, I get to decide what I want the function to do with what kind of data (and only that kind of data). Or, if I am are reading code after someone, I have better idea what kind of things the function might do, or what value I can pass it by reading the function's name and type signature (what type of inputs it consumes and what type of data it gives back). It may seem very restrictive at first but I actually find it very liberating - I don't need to keep in my head all the nuances of what can go wrong when I use a particular function in particular context, or also spend long minutes trying to find out what went wrong and where (eg. I accidentally added int and string, which may have resulted into casting int to string and then string concatenation instead of integer addition). In F# the compiler will help me by not allowing me to do just that. For that matter, if I still want a function to be more "generic", I can use keyword inline in the function definition and compiler will try to infer function's type at each place it is used, instead of just once at its first call.

For example, if I read unit sale price and quantity of units sold from a database, I can make sure (by defining custom types, such as Qty of float and UnitPrice of float and then by specifying those in the function's signature) that my totalSales function will accept only those two types of values and no other - I cannot accidentally pass in two quantities and multiply them (that makes no sense, it would be a bug), or any other numerical value. The compiler would stop me and demand the correct type of values.

Another example, F# allows to define types as units of measure. So you can further improve your code to prevent mistakes - it makes no sense to sum litres, kilograms, pieces and cubic meters. You can refine the Quantity type further and then create functions which ensure nowhere in the codebase you / someone else adds wrong quantities by mistake. Because if function works for litres only and you pass it a value holding kgs, the program will not compile. If you still want to sum kgs and litres, you need to create a function doing the conversion from kgs to litres (using density) and then use the returned value in your sum litres function.

I hope this makes sense and that you find it helpful or interesting :)

Collapse
dewofyouryouth_43 profile image
Jacob E. Shore Author • Edited

Like I've said, I'm not confused about why you can't use the same function to add and concatenate. I just prefer being able to tell what the function does just by looking at the definition. I would rather just assign types to the function itself.

Collapse
epsi profile image
E.R. Nurwijayadi • Edited

Thank you for posting.

I'm a beginner in F#. To leverage beginner skill, I have made a working example with source code in github.

🕷 epsi.bitbucket.io/lambda/2020/12/0...

The concurrency part is based on

🕷 codemag.com/Article/1707051/Writin...

But the rest is my custom script..

Sender and Receiver

I hope this could help other who seeks for other case example.

🙏🏽

Thank you for posting.

Collapse
omanf profile image
O.F.K.

While I haven't read the article in "F# for fun and profit" that Drew Knab referred you to, one thing to take into account when composing vs piping is that, just like math from where the composition is taken from, when composing functions the end result is a single function, the composition of all the functions composed.

Usually, that's what you want, usually, if you pipe several functions together the F# compiler itself will give you a refactoring suggestion to turn a lot of pipes into a chain of compositions.
But every once in a while, you compose instead of pipe and if an issue pops up inside one of those functions, finding it is wayyyy harder than if you piped functions.

It's a game because more often than not composition has performance benefits, but when you need granularity in debugging it can be the difference between "just getting it" and banging your head against the wall (especially seeing as .Net's error messages are not exactly grade-A).