Idiomatic Functional Programming
- Design principles, design patterns, refactoring in Object-Oriented programming
=>
Idioms in Functional Programming - We have touched on much of this so much of this is review
FP Idioms
Here is a list of principles/idioms, many of which are review as we touched on them before
Don’t Repeat Yourself (DRY from OO):
- Extract duplicate code into its own function
- if there is common code except for one spot, make that spot a code (i.e.function) parameter
- example: make a map function on a tree if you are doing many tree operations that are maps.
- Code usually won’t be exact duplicate; extract different bits as function parameters so the different bits are passed in
- May also entail replacing specific types with generic types
'aor functor parameter typest,eltetc
Hide it behind an interface
- Modularity / focus of responsibility: Make clear divisions of responsibility between different modules and functions
- Hiding minimizes what programmer-users have to think about, they can think at the higher (simpler) level of the interface
- and, this again will take up less brain space since they are not seeing lots of low-level details.
- If a function is auxiliary to only one other function, define it in the body of the latter.
- i.e.
let f x = let aux y = ..<aux's body>.. in .. <f's body> ..
- i.e.
- If a function is not local to a single function but is not used outside its module, leave it out of the module type (the
.mlifile) which will hide it to module users - Make a new module for a new data type, and include operations on the type (only) in it
- This is not just for generic data structures like
Map/Set, it is for app-specific data structures - Example:
ChessBoardis a nontrivial data type for a chess game, make it its own module
- This is not just for generic data structures like
- Hide types
tin module types if users don’t need to see the details
Generally avoid side effects
- Recall how pure functional code is referentially transparent, the behavior is all in the interface with no “hidden” actions.
- Side effect world view is a state machine vs functional view as a pipeline explicitly passing data on
- Occasionally side effects will make code more concise, that is the only time to use them
- Functional code has everything in the interface so it will make a more accurate interface
- This is not always good though: consider e.g. imperative
fresh_name : () -> stringmaking a different string each time calledlet counter = ref 0 let fresh_name () : string = (counter := !counter + 1); "name"^(Int.to_string !counter) - To make a fresh string in functional code you would need to e.g. pass the previous count and return the next one.
let fresh_name (previous : int) : (string * int) = ("name"^(Int.to_string previous), previous + 1) let (name1,count1) = fresh_name 0 in let (name1,count2) = fresh_name count1 in ...This is a more accurate interface to what freshness needs, but any function needing to make fresh names would need to be passed and return the current
countvalue. Yuck! - The difficulty for long-time imperative programmers is everything looks like a
fresh_namecase at first, it just seems impossible to write an elegant functional version. Don’t give up, you will learn!
- This is not always good though: consider e.g. imperative
Have a focus of responsibility
- Each function and module should have a clear focus that can be summarized in a sentence
- Divide one function/module into two if it is doing two different things
- Don’t add random stuff to a module if it doesn’t fit with it’s summary purpose
- If you need more than is in an existing module, make a new one and
includethe old one
Concision
“Concise is nice”
- Goal of making code as short as possible
- From the classic Strunk and White English writing guide:
A sentence should contain no unnecessary words, a paragraph no unnecessary sentences, for the same reason that a drawing should have no unnecessary lines and a machine no unnecessary parts
- Concise code means that more information will fit in your brain’s fixed-size working set
Many particular low-level points were already covered in the FPSE Style Guide, here is a review:
- Combinize: replace
let recwithmaps,folds and the like- and, for your own data structures write your own combinators and then use in place of
rec
- and, for your own data structures write your own combinators and then use in place of
- Use advanced pattern matching (
as,with, deep patterns, partial record patterns,_, etc) - Use
|>in place of call sequences, and make your functions amenable to piping- To have pipes be effective, the best way is to follow
Coreand name the parameters that are not the underlying data you will often want to pipe on. For exampleList.filter : 'a list -> f:('a -> bool) -> 'a listand sincefnamed can apply it first to make an'a list -> 'a listfunction ripe for piping:[2;5;-6;22] |> List.filter ~f:(fun x -> x < 0) |> List.is_empty |> not;;
- To have pipes be effective, the best way is to follow
- Use
@@in place of parentheses - Inline simple
letdefinitions to make code read as a concise sentence- Also a small function called only once or twice may read better inlined
- Conversely, make more
letdefinitions if the code is too convoluted
Functional code is not always more concise than stateful code, but it is surprisingly good
- Let us revisit the parenthesis matching example from the previous lecture on side effects.
- At the end of this file is a purely functional version of paren matching; the code is much more concise.
Speed
- There is always a trade-off in programming between efficiency and elegance
- Much of the time it is possible to prioritize concision, modularity, functional coding over efficiency
- Note that Python and JavaScript are ~5-10 times slower than C or OCaml, a case in point for speed not a priority
- But, sometimes speed really matters
- When data sets get large or algorithms get complex
- Do generally avoid high polynomial or exponential algorithms on potentially large inputs
- Also pay more attention when data sets get extremely large, even n vs n log n gets noticeable there.
- a functional
Mapmay need to be replaced with a mutableHashtbl
- a functional
- We won’t cover performance any more today but there is a whole lecture coming up on the topic.
Examples of Idiomatic FP
Here are example codebases we will spend time inspecting and critiqueing.
Minesweeper
- Minesweeper at Exercism.io
- Its not the full game, just calculating the number of mines adjacent to each non-mine square
- Example input/input:
[ " * "; " * "; "*****"; " * "; " * "; ] [ " 2*2 "; "25*52"; "*****"; "25*52"; " 2*2 "; ]See test.ml for more examples
- Before getting into the code let’s consider algorithms, there are two distinct approaches
- For each non-mine square look around it and add up the mines
- Or, for each mine square increment the count on all non-mine squares around it (start at 0)
- The latter is fundamentally more difficult to do functionally
- The grid will need to change (mutate) a great many times as counts bump up one by one
- The former can produce the complete answer for a given cell in one go, no incrementalism
- We will review this functional solution
- We made another variation on the functional version to be cleaner and more efficient
- Will look at an imperative approach which has some poor abstractions and fails to use combinators.
- Before getting into the code let’s consider algorithms, there are two distinct approaches
Other Examples
- ocaml-cuid is a utility to generate highly random string IDs for webpages etc.
- Lots of nice piping here plus use of functors to build Unix and JavaScript variations
- dolog is a very simple logging utility
- Shows some nice use of state,
include, and aMakefunctor.
- Shows some nice use of state,
- We may also look at a past homework solution so you can compare it with what you did.