Learning F# – Part 3

Dis­claimer: I do not claim cred­it for the code exam­ples and much of the con­tents here, these are most­ly 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 includ­ed a lot of code exam­ples for you to try out your­self.

Functions

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­at­ed 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 inte­ger”.

Type Inference

Take this add func­tion for exam­ple:

clip_image002

Look­ing at this you might be won­der­ing why does the com­pil­er 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­it­ly 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­pil­er sim­ply defaults to int if there is no addi­tion­al 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­ev­er, you can pro­vide a type anno­ta­tion, or hint, to the F# com­pil­er 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­pil­er infers y to be a float as well.

Type infer­ence can reduce code clut­ter by hav­ing the com­pil­er fig­ure out what types to use, but the occa­sion­al type anno­ta­tion is required and can some­times improve code read­abil­i­ty.

Generic Functions

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

clip_image005

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

The type of a gener­ic para­me­ter can have the name of any valid iden­ti­fi­er pre­fixed with an apos­tro­phe, but typ­i­cal­ly 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 gener­ic code is impor­tant for max­i­miz­ing code reuse.

Scope

Every val­ue declared in F# has a spe­cif­ic scope, more for­mal­ly 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­ev­er, val­ues defined with­in a func­tion are scoped only to that func­tion.

For exam­ple:

clip_image006

The scop­ing of a vari­able is impor­tant because F# sup­ports nest­ed func­tions — i.e. you can declare new func­tion val­ues with­in the body of a func­tion. Nest­ed func­tions have access to any val­ue declared in a high­er scope as well as any new val­ues declared with­in 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­pil­er error; rather it sim­ply leads to shad­ow­ing. When this hap­pens, both val­ues exists in mem­o­ry, except there is no way to access the pre­vi­ous­ly declared val­ue. For exam­ple:

clip_image008

This tech­nique of inten­tion­al­ly 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 tech­nique.

Control Flow

You can branch con­trol flow using the if key­word which works exact­ly 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 val­ue:

clip_image002[7]

F# has some syn­tac­tic sug­ar to help you com­bat deeply nest­ed if expres­sion with the elif key­word:

clip_image003[6]

Because the result of the if expres­sion is a val­ue, 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­tial­ly “no val­ue”.

Core Types

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

image

Unit

The unit type is a val­ue 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­sent­ed in code via ():

clip_image001[8]

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

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

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

clip_image002[9]