Introduction to Functional Programming with Elan

Getting started

To undertake functional programming (FP) in Elan, you should first be reasonably familiar with procedural programming (PP) with Elan, and ideally, with object-oriented programming (OOP) with Elan. Then, switch to the Functional profile from the menu at the top of the IDE. This changes two things:

In the following sections we will be drawing out the principles, patterns, practices of FP with reference to the various FP demo programs

Principle 1: Functions must be pure

A 'pure' function observes the following rules:

Dedicated functional programmming languages - such as Haskell - enforce these rules automatically. Common 'multi-paradigm' languages - including Python, VB, C#, and Java - do not enforce this rule. However, if any of those languages are used within Elan, then the Elan editor enforces those rules automatically. This means that every function you see within Elan, and every function you have ever written within Elan (in Python, VB, C#, Java), is a pure function. That is the reason why, even when you are using the Procedural or Object Oriented profile and you are within a function you cannot add a print or call procedure, nor make use of system methods such as input, random, or clock. This experience gives you a huge advantage in transitioning to FP.

Principle 2: As much of the code as possible should be in functions

When using FP in Python, VB, C#, or Java, every program still needs a main routine, which is necessarily procedural in nature, because it necessarily has dependencies on the system, and generates side effects such as generating output to a display. And to avoid your main routine becoming difficult to read, and to avoid duplicated code, you may still define procedures, which also, inherently, use procedural programming - to manage input/output and other system operations. But the second principle of FP is that the main, and any procedures, should be kept as small as possible. Any code within the main routine or procedures that performs calculation, evaluates logic, or transforms data in any way, should be delegated to functions. Most procedural programs contain some functions, but a program written within the discipline of FP should consist almost entirely of functions.

This may seem logical when a program is obviously heavily dependent upon calculation, logic, or data transformation - as, for example, the best-fit and Wordle solver demo programs. But what about an interactive game, where most of the logic is concerned with managing the user input and the display? Consider the two demo programs: Snake - procedural and Snake - procedural. The two programs behave identically when run. We recommend that you take the time to explore the these two programs in detail,but here's a quick summary of the key differences:

Snake - procedural Snake - functional
Total instructions, excluding tests 72 115
Number of procedures, including main 4 1
Number of functions 4 21 (incl. function methods within classes)
Instructions within procedures, including main 47 = 65% 11 = 10%
Instructions within functions 25 = 35% 104 = 90%

Principle 3: Every function can and should be unit-tested

The idea of writing automated tests isn't restricted to functional programming. The tests offered by Elan are known as 'unit tests' - designed to test functions. They are extremely easy to write, and run automatically when the program is loaded from a file, and run again after every edit that results in code that compiles. Unit tests give you huge confidence that your code does what it is supposed to do, and that subsequent changes to your code - either as a result of de-bugging, or to implement new requirements - do not break existing functionality, resulting in bugs that might not surface to the user for a long time.

On some other platforms it is possible to write automated tests that also cover procedures (including the main routine). These are sometimes referred to as 'end to end tests', or 'UI tests' because they can simulate a user interacting with the program. But such tests are much harder to write than unit tests, and notoriously 'brittle' - meaning that a tiny change to code can break many existing tests and take hours to fix, even such changes as a routine upgrade to the browser or operating system. Elan does currently support those kind of tests. (Note: outside Elan Python, VB, C#, and Java, unit testing frameworks will, theoretically, allow unit-tests to run any method. This can be dangerous because running the tests might - without your awareness - be updating a file or database, or sending Internet messages. Unit tests are only safe when applied to safe (pure) functions.

It follows that the higher the percentage of a program's total instructions that exist within functions the higher the percentage of your code than can be safely unit tested. The table below shows the percentage of instructions covered by tests for two of the demo programs that offer both procedural and functional implementations. You can verify this information by examining the demo code.

Snake - procedural Snake - functional Life - procedural Life - functional
Total instructions, excluding tests 72 115 78 76
Instructions covered by tests 53% 90% 73% 90%

Notes:

Principle 4: Internally, functions should return an expression, optionally preceded by 'let' instructions

In the FP demos you will see that a significant number of functions simply return return return return return an expression to be evaluated, for example:

+function gameOvername?(g as Gameparameter definitions?) returns BooleanType? 1 return g.body.contains(g.head) or hasHitEdge(g)value or expression?2 end function
+def gameOvername?(g: Gameparameter definitions?) -> boolType?: # function1 return g.body.contains(g.head) or hasHitEdge(g)value or expression?2 main()
+static boolType? gameOvername?(Game gparameter definitions?) { // function1 return g.body.contains(g.head) || hasHitEdge(g)value or expression?;2 }
+Function gameOvername?(g As Gameparameter definitions?) As BooleanType? 1 Return g.body.contains(g.head) Or hasHitEdge(g)value or expression?2 End Function
+static boolType? gameOvername?(Game gparameter definitions?) { // function1 return g.body.contains(g.head) || hasHitEdge(g)value or expression?;2 }

Other functions precede the return return return return return by one or more let statement, for example

+function hasHitEdgename?(g as Gameparameter definitions?) returns BooleanType? 1 let xname? be g.head.xvalue or expression?2 let yname? be g.head.yvalue or expression?3 return (x is -1) or (y is -1) or (x is 40) or (y is 30)value or expression?4 end function
+def hasHitEdgename?(g: Gameparameter definitions?) -> boolType?: # function1 xname? = g.head.xvalue or expression? # let2 yname? = g.head.yvalue or expression? # let3 return (x == -1) or (y == -1) or (x == 40) or (y == 30)value or expression?4 main()
+static boolType? hasHitEdgename?(Game gparameter definitions?) { // function1 var xname? = g.head.xvalue or expression?; // let2 var yname? = g.head.yvalue or expression?; // let3 return (x == -1) || (y == -1) || (x == 40) || (y == 30)value or expression?;4 }
+Function hasHitEdgename?(g As Gameparameter definitions?) As BooleanType? 1 Dim xname? = g.head.xvalue or expression? ' let2 Dim yname? = g.head.yvalue or expression? ' let3 Return (x = -1) Or (y = -1) Or (x = 40) Or (y = 30)value or expression?4 End Function
+static boolType? hasHitEdgename?(Game gparameter definitions?) { // function1 var xname? = g.head.xvalue or expression?; // let2 var yname? = g.head.yvalue or expression?; // let3 return (x == -1) || (y == -1) || (x == 40) || (y == 30)value or expression?;4 }

A let statement works like a variable definition except that the named value created may not be re-assigned. But it differs from a constant in that:

A let statement defines a 'sub-expression' that will be used in another expression, for one (or more) of these reasons:

When running in the Functional profile, let statement is the only type of instruction that you may add within a function, and the only type in addition to an assert that may be added into a test. (You may still add comments.)

Principle 5: Selection is implemented using 'if expressions'

When running in the Functional profile, you cannot add an if instruction into a function. So how do you implement 'selection' and 'iteration'? Iteration will be addressed in the next heading, but selection is handled by means of a 'conditional expression'.

All of the languages supported by Elan have their own, custom syntax for conditional expressions. For simplicity, and understandability, Elan offers an if function that is used identically in all the languages. Incidentally, the if function works the same way that it does in an Excel spreadsheets (though 'IF', like all Excel functions, is capitalised, therein). Here are some examples of conditional expressions from the demo programs:

+function betterOfname?(wc1 as WordCount, wc2 as WordCount, possAnswers as List<of String>parameter definitions?) returns WordCountType? 1 let isBettername? be wc2.count < wc1.countvalue or expression?2 let isEqualAndPossAnswername? be (wc2.count is wc1.count) and possAnswers.contains(wc2.word)value or expression?3 return if(isBetter or isEqualAndPossAnswer, wc2, wc1)value or expression?4 end function
+def betterOfname?(wc1: WordCount, wc2: WordCount, possAnswers: list[str]parameter definitions?) -> WordCountType?: # function1 isBettername? = wc2.count < wc1.countvalue or expression? # let2 isEqualAndPossAnswername? = (wc2.count == wc1.count) and possAnswers.contains(wc2.word)value or expression? # let3 return if(isBetter or isEqualAndPossAnswer, wc2, wc1)value or expression?4 main()
+static WordCountType? betterOfname?(WordCount wc1, WordCount wc2, List<string> possAnswersparameter definitions?) { // function1 var isBettername? = wc2.count < wc1.countvalue or expression?; // let2 var isEqualAndPossAnswername? = (wc2.count == wc1.count) && possAnswers.contains(wc2.word)value or expression?; // let3 return if(isBetter || isEqualAndPossAnswer, wc2, wc1)value or expression?;4 }
+Function betterOfname?(wc1 As WordCount, wc2 As WordCount, possAnswers As List(Of String)parameter definitions?) As WordCountType? 1 Dim isBettername? = wc2.count < wc1.countvalue or expression? ' let2 Dim isEqualAndPossAnswername? = (wc2.count = wc1.count) And possAnswers.contains(wc2.word)value or expression? ' let3 Return if(isBetter Or isEqualAndPossAnswer, wc2, wc1)value or expression?4 End Function
+static WordCountType? betterOfname?(WordCount wc1, WordCount wc2, List<String> possAnswersparameter definitions?) { // function1 var isBettername? = wc2.count < wc1.countvalue or expression?; // let2 var isEqualAndPossAnswername? = (wc2.count == wc1.count) && possAnswers.contains(wc2.word)value or expression?; // let3 return if(isBetter || isEqualAndPossAnswer, wc2, wc1)value or expression?;4 }
+function nextCellValuename?(grid as List<of List<of Int>>, x as Int, y as Intparameter definitions?) returns IntType? 1 let livename? be willLive(grid[x][y], liveNeighbours(grid, x, y))value or expression?2 return if(live, black, white)value or expression?3 end function
+def nextCellValuename?(grid: list[list[int]], x: int, y: intparameter definitions?) -> intType?: # function1 livename? = willLive(grid[x][y], liveNeighbours(grid, x, y))value or expression? # let2 return if(live, black, white)value or expression?3 main()
+static intType? nextCellValuename?(List<List<int>> grid, int x, int yparameter definitions?) { // function1 var livename? = willLive(grid[x][y], liveNeighbours(grid, x, y))value or expression?; // let2 return if(live, black, white)value or expression?;3 }
+Function nextCellValuename?(grid As List(Of List(Of Integer)), x As Integer, y As Integerparameter definitions?) As IntegerType? 1 Dim livename? = willLive(grid[x][y], liveNeighbours(grid, x, y))value or expression? ' let2 Return if(live, black, white)value or expression?3 End Function
+static intType? nextCellValuename?(List<List<int>> grid, int x, int yparameter definitions?) { // function1 var livename? = willLive(grid[x][y], liveNeighbours(grid, x, y))value or expression?; // let2 return if(live, black, white)value or expression?;3 }

The if function requires three arguments to be provided, in the following order:

  1. The condition. This condition may compare two values, or may call another function that returns a Boolean value
  2. The value to be returned by the if function when the condition evaluates to true
  3. The value to be returned by the if function when the condition evaluates to false

The if function, then, performs a role somewhat similar to an if instruction, but differs from it in these important respects:

if functions can be nested. However, nesting more than one level can become very hard to read. In this circumstance it is often better to delegate to new function that perform part of the logic.

Principle 6: Iteration may be achieved using recursion

There are two ways to achieve implement iteration within a function. The first is using 'recursion' - also known as 'recursive' functions). (The second approach is addressed in the next heading.)

A recursive function is best understood by looking at a classic example - the calculation of a factorial:

+function factorialname?(n as Intparameter definitions?) returns IntType? 1 return if(n < 2, 1, n*factorial(n - 1))value or expression?2 end function
+def factorialname?(n: intparameter definitions?) -> intType?: # function1 return if(n < 2, 1, n*factorial(n - 1))value or expression?2 main()
+static intType? factorialname?(int nparameter definitions?) { // function1 return if(n < 2, 1, n*factorial(n - 1))value or expression?;2 }
+Function factorialname?(n As Integerparameter definitions?) As IntegerType? 1 Return if(n < 2, 1, n*factorial(n - 1))value or expression?2 End Function
+static intType? factorialname?(int nparameter definitions?) { // function1 return if(n < 2, 1, n*factorial(n - 1))value or expression?;2 }

This simple example illustrates the three key principles of recursive functions:

Principle 7: Lists and other data structures are never mutated

In procedural programming a list may be mutated either by re-assigning an indexed value, for example:

set mySubjects[3]variableName? to "Geography"value or expression?0
mySubjects[3]variableName? = "Geography"value or expression? # re-assign variable0
mySubjects[3]variableName? = "Geography"value or expression?; // re-assign variable0
mySubjects[3]variableName? = "Geography"value or expression? ' re-assign variable0
mySubjects[3]variableName? = "Geography"value or expression?; // re-assign variable0
or by calling one of the available procedure methods on on it such as:
call mySubjects.appendprocedureName?("Music"arguments?)0
mySubjects.appendprocedureName?("Music"arguments?) # call procedure0
mySubjects.appendprocedureName?("Music"arguments?); // call procedure0
mySubjects.appendprocedureName?("Music"arguments?) ' call procedure0
mySubjects.appendprocedureName?("Music"arguments?); // call procedure0
but in FP this is not permitted. Data structures are never to be mutated. Instead, you may call a function that returns a copy of the data structures, but with specified differences from the original. For example, you may write:
let myNewSubjects be mySubjects.withSet(3, "Geography") Code does not parse as Elan.
let myNewSubjects be mySubjects.withSet(3, "Geography") Code does not parse as Elan.
let myNewSubjects be mySubjects.withSet(3, "Geography") Code does not parse as Elan.
let myNewSubjects be mySubjects.withSet(3, "Geography") Code does not parse as Elan.
let myNewSubjects be mySubjects.withSet(3, "Geography") Code does not parse as Elan.
or
let myNewSubjects be mySubjects.withAppend("Music") Code does not parse as Elan.
let myNewSubjects be mySubjects.withAppend("Music") Code does not parse as Elan.
let myNewSubjects be mySubjects.withAppend("Music") Code does not parse as Elan.
let myNewSubjects be mySubjects.withAppend("Music") Code does not parse as Elan.
let myNewSubjects be mySubjects.withAppend("Music") Code does not parse as Elan.

At first sight this seem awkward, strange, and certainly inefficient. But though you might never have thought about it in these terms, this is what you have always done when manipulating a String str string String String. You can never actually mutate a string - such as converting it to upper case or trimming off the leading spaces, you have to write, for example:

let myNewString be myString. "Geography") Code does not parse as Elan.
let myNewString be myString. "Geography") Code does not parse as Elan.
let myNewString be myString. "Geography") Code does not parse as Elan.
let myNewString be myString. "Geography") Code does not parse as Elan.
let myNewString be myString. "Geography") Code does not parse as Elan.
Merge Sort demo, which, as well as demonstrating recursion, is a good example of functional decomposition: the principal function, sort delegates work, directly or indirectly to four other functions - sortedFrontHalf, sortedBackHalf, merge, and mergeNonEmpty each of which has its own associated unit tests. This makes the code easier to read, and possible to test progressively.

sort and merge are both recursive. This might not be immediately apparent, because they don't make use of themselves directly. But sort delegates work to sortedFrontHalf and sortedBackHalf, both of which in turn make use of the sort:

+function sortname?(li as List<of String>parameter definitions?) returns List<of String>Type? 1 return if(li.length() < 2, li, merge(sortedFrontHalf(li), sortedBackHalf(li)))value or expression?2 end function +function sortedFrontHalfname?(li as List<of String>parameter definitions?) returns List<of String>Type? 3 let midname? be divAsInt(li.length(), 2)value or expression?4 let frontHalfname? be li.subList(0, mid)value or expression?5 return sort(frontHalf)value or expression?6 end function +function sortedBackHalfname?(li as List<of String>parameter definitions?) returns List<of String>Type? 7 let midname? be divAsInt(li.length(), 2)value or expression?8 let backHalfname? be li.subList(mid, li.length())value or expression?9 return sort(backHalf)value or expression?10 end function
+def sortname?(li: list[str]parameter definitions?) -> list[str]Type?: # function1 return if(li.length() < 2, li, merge(sortedFrontHalf(li), sortedBackHalf(li)))value or expression?2 +def sortedFrontHalfname?(li: list[str]parameter definitions?) -> list[str]Type?: # function3 midname? = divAsInt(li.length(), 2)value or expression? # let4 frontHalfname? = li.subList(0, mid)value or expression? # let5 return sort(frontHalf)value or expression?6 +def sortedBackHalfname?(li: list[str]parameter definitions?) -> list[str]Type?: # function7 midname? = divAsInt(li.length(), 2)value or expression? # let8 backHalfname? = li.subList(mid, li.length())value or expression? # let9 return sort(backHalf)value or expression?10 main()
+static List<string>Type? sortname?(List<string> liparameter definitions?) { // function1 return if(li.length() < 2, li, merge(sortedFrontHalf(li), sortedBackHalf(li)))value or expression?;2 } +static List<string>Type? sortedFrontHalfname?(List<string> liparameter definitions?) { // function3 var midname? = divAsInt(li.length(), 2)value or expression?; // let4 var frontHalfname? = li.subList(0, mid)value or expression?; // let5 return sort(frontHalf)value or expression?;6 } +static List<string>Type? sortedBackHalfname?(List<string> liparameter definitions?) { // function7 var midname? = divAsInt(li.length(), 2)value or expression?; // let8 var backHalfname? = li.subList(mid, li.length())value or expression?; // let9 return sort(backHalf)value or expression?;10 }
+Function sortname?(li As List(Of String)parameter definitions?) As List(Of String)Type? 1 Return if(li.length() < 2, li, merge(sortedFrontHalf(li), sortedBackHalf(li)))value or expression?2 End Function +Function sortedFrontHalfname?(li As List(Of String)parameter definitions?) As List(Of String)Type? 3 Dim midname? = divAsInt(li.length(), 2)value or expression? ' let4 Dim frontHalfname? = li.subList(0, mid)value or expression? ' let5 Return sort(frontHalf)value or expression?6 End Function +Function sortedBackHalfname?(li As List(Of String)parameter definitions?) As List(Of String)Type? 7 Dim midname? = divAsInt(li.length(), 2)value or expression? ' let8 Dim backHalfname? = li.subList(mid, li.length())value or expression? ' let9 Return sort(backHalf)value or expression?10 End Function
+static List<String>Type? sortname?(List<String> liparameter definitions?) { // function1 return if(li.length() < 2, li, merge(sortedFrontHalf(li), sortedBackHalf(li)))value or expression?;2 } +static List<String>Type? sortedFrontHalfname?(List<String> liparameter definitions?) { // function3 var midname? = divAsInt(li.length(), 2)value or expression?; // let4 var frontHalfname? = li.subList(0, mid)value or expression?; // let5 return sort(frontHalf)value or expression?;6 } +static List<String>Type? sortedBackHalfname?(List<String> liparameter definitions?) { // function7 var midname? = divAsInt(li.length(), 2)value or expression?; // let8 var backHalfname? = li.subList(mid, li.length())value or expression?; // let9 return sort(backHalf)value or expression?;10 }
Similarly merge delegates to mergeNonEmpty, which in turn makes use of merge:
+function mergename?(a as List<of String>, b as List<of String>parameter definitions?) returns List<of String>Type? 1 let oneIsEmptyname? be (a.length() is 0) or (b.length() is 0)value or expression?2 return if(oneIsEmpty, a.withAppendList(b), mergeNonEmpty(a, b))value or expression?3 end function +function mergeNonEmptyname?(a as List<of String>, b as List<of String>parameter definitions?) returns List<of String>Type? 4 let aHeadname? be a.head()value or expression?5 let bHeadname? be b.head()value or expression?6 let aTailname? be a.tail()value or expression?7 let bTailname? be b.tail()value or expression?8 return if(aHead.isBefore(bHead), [aHead].withAppendList(merge(aTail, b)), [bHead].withAppendList(merge(a, bTail)))value or expression?9 end function
+def mergename?(a: list[str], b: list[str]parameter definitions?) -> list[str]Type?: # function1 oneIsEmptyname? = (a.length() == 0) or (b.length() == 0)value or expression? # let2 return if(oneIsEmpty, a.withAppendList(b), mergeNonEmpty(a, b))value or expression?3 +def mergeNonEmptyname?(a: list[str], b: list[str]parameter definitions?) -> list[str]Type?: # function4 aHeadname? = a.head()value or expression? # let5 bHeadname? = b.head()value or expression? # let6 aTailname? = a.tail()value or expression? # let7 bTailname? = b.tail()value or expression? # let8 return if(aHead.isBefore(bHead), [aHead].withAppendList(merge(aTail, b)), [bHead].withAppendList(merge(a, bTail)))value or expression?9 main()
+static List<string>Type? mergename?(List<string> a, List<string> bparameter definitions?) { // function1 var oneIsEmptyname? = (a.length() == 0) || (b.length() == 0)value or expression?; // let2 return if(oneIsEmpty, a.withAppendList(b), mergeNonEmpty(a, b))value or expression?;3 } +static List<string>Type? mergeNonEmptyname?(List<string> a, List<string> bparameter definitions?) { // function4 var aHeadname? = a.head()value or expression?; // let5 var bHeadname? = b.head()value or expression?; // let6 var aTailname? = a.tail()value or expression?; // let7 var bTailname? = b.tail()value or expression?; // let8 return if(aHead.isBefore(bHead), [aHead].withAppendList(merge(aTail, b)), [bHead].withAppendList(merge(a, bTail)))value or expression?;9 }
+Function mergename?(a As List(Of String), b As List(Of String)parameter definitions?) As List(Of String)Type? 1 Dim oneIsEmptyname? = (a.length() = 0) Or (b.length() = 0)value or expression? ' let2 Return if(oneIsEmpty, a.withAppendList(b), mergeNonEmpty(a, b))value or expression?3 End Function +Function mergeNonEmptyname?(a As List(Of String), b As List(Of String)parameter definitions?) As List(Of String)Type? 4 Dim aHeadname? = a.head()value or expression? ' let5 Dim bHeadname? = b.head()value or expression? ' let6 Dim aTailname? = a.tail()value or expression? ' let7 Dim bTailname? = b.tail()value or expression? ' let8 Return if(aHead.isBefore(bHead), {aHead}.withAppendList(merge(aTail, b)), {bHead}.withAppendList(merge(a, bTail)))value or expression?9 End Function
+static List<String>Type? mergename?(List<String> a, List<String> bparameter definitions?) { // function1 var oneIsEmptyname? = (a.length() == 0) || (b.length() == 0)value or expression?; // let2 return if(oneIsEmpty, a.withAppendList(b), mergeNonEmpty(a, b))value or expression?;3 } +static List<String>Type? mergeNonEmptyname?(List<String> a, List<String> bparameter definitions?) { // function4 var aHeadname? = a.head()value or expression?; // let5 var bHeadname? = b.head()value or expression?; // let6 var aTailname? = a.tail()value or expression?; // let7 var bTailname? = b.tail()value or expression?; // let8 return if(aHead.isBefore(bHead), [aHead].withAppendList(merge(aTail, b)), [bHead].withAppendList(merge(a, bTail)))value or expression?;9 }

These examples demonstrate two key principles of

Principle 7: A function may be passed into, or returned by, another function # Higher order functions - Use of Hofs - passing in a function name - defining a lambda

Principle 8: Data structures should never be mutated

- standard data structures - `with` methods return a copy with specified differences - with methods may be chained. (Maybe add graphics into best-fit demo?) - simple examples - ser-defined types - Classes can only have function methods, no procedures - 'updates' (like strings, or lists) are done by returning a copy with the changes - 'with' instructions are just a special case of a function method, with own template, and code and a 'set' which you should not delete.

Patterns and Practices

Generating Random numbers within functions

Manipulating 2D lists