Dis­claimer: I do not claim credit for the code exam­ples and much of the con­tents here, these are mostly extracts from the book by Chris Smith, Pro­gram­ming F#: A com­pre­hen­sive guide for writ­ing sim­ple code to solve com­plex prob­lems. In fact, if you’re think­ing of learn­ing F# and like what you read here, you should buy the book your­self, it’s easy to read and the author has gone go great lengths to keep things sim­ple and included a lot of code exam­ples for you to try out yourself.

Func­tions

You define func­tions the same way you define val­ues, except every­thing after the name of the func­tion servers as the function’s para­me­ters. The fol­low­ing defines a func­tion called square that takes an inte­ger, x, and returns its square:

clip_image001

Unlike C#, F# has no return key­word. The last expres­sion to be eval­u­ated in the func­tion deter­mines the return type.

Also, from the FSI out­put above, it shows the func­tion square has sig­na­ture int -> int, which reads as “a func­tion tak­ing an inte­ger and return­ing an integer”.

Type Infer­ence

Take this add func­tion for example:

clip_image002

Look­ing at this you might be won­der­ing why does the com­piler think that the add func­tion only takes inte­gers? The + oper­a­tor also works on floats too!

The rea­son is type infer­ence. Unlike C#, F# doesn’t require you to explic­itly state the types of all the para­me­ters to a func­tion, it fig­ures it out based on usage. Because the + oper­a­tor works for many dif­fer­ent types such as byte, int, and dec­i­mal, the com­piler sim­ply defaults to int if there is no addi­tional infor­ma­tion.

The fol­low­ing FSI snip­pet shows what type infer­ence in action if we not only define the add func­tion but also call it pass­ing in floats, then the function’s sig­na­ture will be inferred to be of type float -> float -> float instead:

clip_image003

How­ever, you can pro­vide a type anno­ta­tion, or hint, to the F# com­piler about what the types are. To do this, sim­ply replace a func­tion para­me­ter with the fol­low­ing form ident -> (ident: type) like this:

clip_image004

This works because the only over­load for + that takes a float as its first para­me­ter is float -> float -> float, so the F# com­piler infers y to be a float as well.

Type infer­ence can reduce code clut­ter by hav­ing the com­piler fig­ure out what types to use, but the occa­sional type anno­ta­tion is required and can some­times improve code readability.

Generic Func­tions

You can write func­tions that work for any type of a para­me­ter, such as an iden­tity func­tion below:

clip_image005

Because the type infer­ence sys­tem could not deter­mine a fixed type for value x in the ident func­tion, it was generic. If a para­me­ter is generic, then that para­me­ter can be of any type.

The type of a generic para­me­ter can have the name of any valid iden­ti­fier pre­fixed with an apos­tro­phe, but typ­i­cally let­ters of the alpha­bet start­ing with ‘a’ as you can see from the FSI snip­pet for the ident func­tion above.

Writ­ing generic code is impor­tant for max­i­miz­ing code reuse.

Scope

Every value declared in F# has a spe­cific scope, more for­mally referred to as a dec­la­ra­tion space.

The default scope is mod­ule scope, mean­ing vari­ables can be used any­where after their dec­la­ra­tion. How­ever, val­ues defined within a func­tion are scoped only to that function.

For exam­ple:

clip_image006

The scop­ing of a vari­able is impor­tant because F# sup­ports nested func­tions — i.e. you can declare new func­tion val­ues within the body of a func­tion. Nested func­tions have access to any value declared in a higher scope as well as any new val­ues declared within itself. The fol­low­ing exam­ples shows this in action:

clip_image007

In F#, hav­ing two val­ues with the same name doesn’t lead to a com­piler error; rather it sim­ply leads to shad­ow­ing. When this hap­pens, both val­ues exists in mem­ory, except there is no way to access the pre­vi­ously declared value. For example:

clip_image008

This tech­nique of inten­tion­ally shad­ow­ing val­ues is use­ful for giv­ing the illu­sion of updat­ing val­ues with­out rely­ing on muta­tion. Think strings in C#, which is an immutable type that allows reas­sign­ment using the same shad­ow­ing technique.

Con­trol Flow

You can branch con­trol flow using the if key­word which works exactly like an if state­ment in C#:

clip_image001[6]

F# sup­ports if-then-else struc­ture, but the thing the sets if state­ments in F# apart is that if expres­sions return a value:

clip_image002[7]

F# has some syn­tac­tic sugar to help you com­bat deeply nested if expres­sion with the elif keyword:

clip_image003[6]

Because the result of the if expres­sion is a value, every clause of an if expres­sion must return the same type.

But if you only have a sin­gle if and no cor­re­spond­ing else, then the clause must return unit, which is a spe­cial type in F# that means essen­tially “no value”.

Core Types

Besides the prim­i­tive types, the F# library includes sev­eral core types that will allow you to orga­nize, manip­u­late and process data:

image

Unit

The unit type is a value sig­ni­fy­ing noth­ing of con­se­quence. unit can be thought of as a con­crete rep­re­sen­ta­tion of void and is rep­re­sented in code via ():

clip_image001[8]

if expres­sions with­out a match­ing else must return unit because if they did return a value, what would hap­pen if else was hit?

Also, in F#, every func­tion must return a value, think method in C# and the void return type, so even if the func­tion doesn’t con­cep­tu­ally return any­thing then it should return a unit value.

The ignore func­tion can swal­low a function’s return value if you want to return unit:

clip_image002[9]

Share

Leave a Reply