I recently took a look at the book "Functional Programming in Java", 2nd Edition, by Venkat Subramaniam. The focus of the book is on Java's lambda expressions and streams library, which leads me to wonder: what does Fantom have to say on these ideas? Fantom has closures and rich collection libraries. I thought it would be interesting to review Fantom's abilities in this area, using the early part of the above book as a guide; the later part of the book considers more general design questions or Java-specific material, some of which is also applicable to Fantom code.
The first example from the book aims to highlight the difference between a procedural and more functional solution to the same problem: take a list of prices, and then calculate the total of the discounted rate of items valued over 20:
A procedural Fantom version may look like this:
class Main { static Void main(Str[] args) { prices := Int[10, 30, 17, 20, 18, 45, 12] totalOfDiscountedPrices := 0.0f for (i := 0; i < prices.size; i += 1) { if (prices[i] > 20) { totalOfDiscountedPrices += prices[i] * 0.9f } } echo("Total of discounted prices: $totalOfDiscountedPrices") } }
The functional version relies on common operations working directly on lists, using a closure (an anonymous function) as an argument to specify the required behaviour of each list method for this problem:
prices := Int[10, 30, 17, 20, 18, 45, 12] totalOfDiscountedPrices := prices .findAll { it > 20 } // <1> .map |Int x -> Float| { x * 0.9f } // <2> .reduce(0.0f) |Float r, Float v->Float| { return r + v } // <3> echo("Total of discounted prices: $totalOfDiscountedPrices")
- The "findAll" method tests each item against an it-block.
- Types are explicitly provided to the "map" function's argument, to ensure the list is converted to one of the expected type.
- Finally, "reduce" collapses a list of items into a single value, based on the given function.
As we can see, the intent of the code is somewhat clearer from the top-level
methods: findAll
finds prices with the given condition, map
converts
items using a formula, and reduce
combines the items into a single result.
Fantom's syntax is very clean here: methods do not need parentheses if there
are no arguments, and, when the last argument is a function, it can be written
"outside" the usual argument list - see reduce
, which takes the initial
item and the reducer function as its two arguments.
You may be concerned about efficiency: I expect the fact that each Fantom operation creates a new list will mean the functional-style will be inherently less efficient, which may be something you need to watch.
A typical test of a language's functional credentials is to see if functions are "first-class" values: in Fantom's case, this is true. What do we mean by "first-class" values? Definitions vary a little, but the key properties are that you can:
- reference a function with a variable;
- pass a function to another function (or method) as a parameter; and
- return a function from another function (or method) as a result.
Here's an example of this. Two functions are created and given names sum
and
diff
. These can then be passed to another function (method) called doCalc
,
which applies the passed function to some values and returns a result. The code
also illustrates how the functions can be created directly at the time of the
call to doCalc
. The makeAdder
method accepts one argument, and then returns
a function which accepts a second argument and returns the sum of the two
arguments.
class ExampleClosure { static Void main(Str[] args) { sum := |Int a, Int b -> Int| { return a + b } // <1> diff := |Int a, Int b -> Int| { return a - b } echo("Sum: " + doCalc(10, 2, sum)) // <2> echo("Diff: " + doCalc(10, 2, diff)) echo("Times: " + doCalc(10, 2, |Int a, Int b -> Int| { return a * b })) echo("Divide: " + doCalc(10, 2) |Int a, Int b -> Int| { return a / b }) add1 := makeAdder(1) // <3> add2 := makeAdder(2) echo("Add 1: " + add1(10)) // <4> echo("Add 2: " + add2(10)) } // This method accepts a function of two numbers, and returns the result of // applying this function. static Int doCalc(Int a, Int b, |Int,Int -> Int| calc) // <5> { return calc(a, b) // <6> } // This method takes a number and returns a function which uses that number. static |Int -> Int| makeAdder(Int x) // <7> { return |Int y -> Int| { return x + y } // <8> } }
-
Create a function and assign it to a variable,
sum
. -
Pass the function as a parameter to the
doCalc
method. Notice how the function can be passed using a variable, directly as a value, and that the function argument can be passed from outside the argument parentheses. - Call a method which returns a function, and store the result.
- Apply a function stored in a variable reference.
- Last parameter is a function value.
- Applies the provided function value to compute a result.
- Notice the return type: returns a function value.
- Create and return a function value.
Functions are "closures", which means they can reference variables in their environment. Hence, this technique for adding the numbers in a list:
items := Int[1,2,3,4,5] // <1> sum := 0 // <2> items.each |Int x| // <3> { sum += x // <4> } echo("Sum is: $sum") // <5>
- Create a list to iterate over.
- A local variable to hold the result.
-
Pass a closure to the
each
method defined on theList
collection. -
Within the closure, modify the local variable
sum
. - Show the final result, which should be 15.
The List is an ordered collection of items, indexed by an integer.
There are five kinds of operations supported on lists which accept functions as one of their parameters, distinguished largely by the kind of result obtained from the function when applied to all items in the list in turn:
Kind of Operation | Kind of Result | Methods on List |
---|---|---|
collapse to value | return a single overall value from function results |
all any join max min reduce
|
iteration | applies function to all, or sublist of, items |
each eachr eachNotNull eachRange
|
search | return a new value or list of list values based on function results |
binaryFind binarySearch eachWhile eachrWhile exclude find findAll findIndex
|
split | return multiple results based on function results |
groupBy groupByInto
|
transform | return a list of values based on function results |
flatMap map mapNotNull sort sortr
|
Methods in this group apply a function to all items in the list, but "collapse" the results into a single value.
The basic method is reduce
- in fact, this method is so powerful you can
define most, if not all, the other methods described on this page in terms of
it.
The reduce
method takes two arguments: an initial value, which can be any
kind of value, and a function. The idea is that the function combines a partial
result with the next list item to obtain a new partial result: starting from
the initial value, the function is applied to each list item in turn, finally
returning the overall result. To achieve this, the function itself takes two
arguments: something of the same type as the initial value (the partial
result), and the next item in the list. The function returns something of the
same type as the initial value.
For example, consider how to add up a list of integers: the initial value will be 0, and the function will take the current sum, and return a new sum consisting of the current sum added to the next item in the list:
fansh> [1,2,3,4,5].reduce(0) |Int sum, Int item -> Int| { return sum + item } 15
The initial value can be a more complex type: let's consider returning a map of each integer linked to its square:
fansh> [1,2,3,4,5].reduce(Int:Int[:]) |Int:Int squares, Int item -> Int:Int| { return squares[item] = item*item } [1:1, 2:4, 3:9, 4:16, 5:25]
The following methods are special applications of reduce
:
-
all
- returns true if the given function returns true for all items in the listfansh> [1,2,3,4,5].all { isOdd } false fansh> [1,2,3,4,5].all { it < 6 } true
-
any
- returns true if the given function returns true for at least one item in the listfansh> [1,2,3,4,5].any { isOdd } true
-
join
- returns a string formed by joining a string representation of each item using given separator. If a function is provided, it is used to provide each item's string, otherwisetoStr
is called.fansh> [1,2,3,4,5].join(" ") 1 2 3 4 5 fansh> [1,2,3,4,5].join(",") { "< $it >" } < 1 >,< 2 >,< 3 >,< 4 >,< 5 >
-
max
- returns the largest element in the list, using the given function to define "largest". Notice in this case the function receives two items to compare, and returns the usual comparator values of -1, 0, or 1.fansh> [1,-3,-7,4,-2].max |Int a, Int b -> Int| {return a.abs <=> b.abs } -7
-
min
- returns the smallest element in the list, using the given function to define "largest". Notice in this case the function receives two items to compare, and returns the usual comparator values of -1, 0, or 1.fansh> [1,-3,-7,4,-2].min |Int a, Int b -> Int| {return a.abs <=> b.abs } 1
Iteration applies the given function to all, or a sublist of, the items in the list. There is no specific return value, and the function is assumed to generate side-effects. The function receives two arguments: the item in the list and the item's index position in the list.
The basic method here is each
, which applies a given function to every item
in the list:
fansh> s := Str["a", "b", "c", "d"] [a, b, c, d] fansh> s.each |Str item, Int index| { echo("Item $item at $index") } Item a at 0 Item b at 1 Item c at 2 Item d at 3
As Fantom allows us to use functions with less than the expected number of arguments,
our call to each
can be restricted to just the item, and this can be the focus of
an "it-block":
fansh> s.each |Str item| { echo("Item is $item") } Item is a Item is b Item is c Item is d fansh> s.each { echo("Item is $it") } Item is a Item is b Item is c Item is d
Two small variations of each
are eachr
and eachNotNull
:
-
eachr
iterates through the list in reverse order:fansh> s.eachr |Str item, Int index| { echo("Item $item at $index") } Item d at 3 Item c at 2 Item b at 1 Item a at 0
-
eachNotNull
iterates forwards, skipping over any null items:fansh> t := Str?["a", null, "b", "c", null] [a, null, b, c, null] fansh> t.each { echo(it) } a null b c null fansh> t.eachNotNull { echo(it) } a b c
The final method restricts iteration to a sub-part of the list:
-
eachRange
iterates over a given range of indices:fansh> t.eachRange(2..3) { echo(it) } b c
Search methods use their function to locate one or more items in the list.
The two basic methods here are find
and findAll
. They each use a boolean
function accepting two arguments, the usual item in the list and its index
position. However, the first returns the first item in the list where the
function returns true, and the second returns a new list of all items in the
list where the function returns true.
fansh> [1,2,3,4,5].find { isEven } 2 fansh> [1,2,3,4,5].findAll { isEven } [2, 4] fansh> [1,2,3,4,5].find { it > 6 } null fansh> [1,2,3,4,5].findAll { it > 6 } [,]
The following two methods are variations on the basic methods:
-
exclude
is the inverse offindAll
, returning all items for which the function returns falsefansh> [1,2,3,4,5].exclude { isEven } [1, 3, 5]
-
findIndex
is likefind
but returns the found item's index instead of the item itselffansh> [1,2,3,4,5].findIndex { isEven } 1 fansh> [1,2,3,4,5].findIndex { it > 6 } null
The following two methods are also related to find
, but they search on the
function's computed value, stopping when a non-null computed value is found:
-
eachWhile
iterates through a list, returning the first non-null value returned by the function. -
eachrWhile
works in the same way, but starts iterating from the end of the list.fansh> tester := |Int x -> Int?| { if (x.isOdd) return x; else return null; } |sys::Int->sys::Int?| fansh> tester(3) 3 fansh> tester(2) null fansh> [2,4,1,3,2].eachWhile(tester) 1 fansh> [2,4,6,8].eachWhile(tester) null fansh> [2,4,1,3,2].eachrWhile(tester) 3
Finally, in the case where your list is ordered, there are two efficient search algorithms.
-
binaryFind
- given a function accepting a list item and its index, returns a comparator value indicating if the given item is less, equal or greater than the target. The final result is the index of the found item, or a negative number if the target is not found.
In the following example, the first attempt looks for the position of "3", and returns its index of 1. The second attempt looks for the position of "4", but fails to find it, returning -3.
fansh> [1,3,5,7,9].binaryFind |Int item->Int| { return 3 <=> item } 1 fansh> [1,3,5,7,9].binaryFind |Int item->Int| { return 4 <=> item } -3
-
binarySearch
- searches for a given target value, with an optional comparator function specifying the order of the list
In the following example, the list of strings is ordered by size, so the comparator function to compare items by size is provided:
fansh> ["a", "g", "cat", "badger"].binarySearch("cat") -2 fansh> ["a", "g", "cat", "badger"].binarySearch("cat") |Str a, Str b->Int| { return a.size <=> b.size } 2
The "split" group of methods divide a list into sublists based on the results of a function: all items in the list for which the function produces the same value are placed into the same group. The groups are stored in a map, with the function value as the key and list of items as its value.
The basic method here is groupBy
- as with each
, the provided function
is passed two arguments, the item in the list and its index:
fansh> [1,2,3,4,5].groupBy { isOdd } [false:[2, 4], true:[1, 3, 5]]
The isOdd
method is called on each item in the list: it returns false
for
even numbers and true
for odd numbers, and you can see those items in the
returned map.
An extension is groupByInto
, which extends an existing map - in this example,
we group words by their size:
fansh> results := ["ape", "bear", "cat", "deer"].groupBy { size } [3:[ape, cat], 4:[bear, deer]] fansh> ["elephant", "fox", "gorilla", "hare"].groupByInto(results) { size } [3:[ape, cat, fox], 4:[bear, deer, hare], 7:[gorilla], 8:[elephant]]
Notice from the call to groupByInto
that "fox" is added to the group for 3,
"hare" to the group for 4, and new groups are added.
The "transform" group of methods apply a function to each item in the list and return a list of results.
There are two sets of methods in this group: the first return new lists of values using the results of a function, which receives each item in the list and its index position in turn; the second rearrange (sort) the items in the list using an optional comparator function.
The basic function for this group is map
, which returns a new list of values:
fansh> [1,2,3,4,5].map { it*it } [1, 4, 9, 16, 25] fansh> [1,2,3,4,5].map |Int item, Int index->Str| { return "[$index] => $item" } [[0] => 1, [1] => 2, [2] => 3, [3] => 4, [4] => 5]
Two variations make minor modifications to the list of returned values:
-
flatMap
is the same asmap
except that any list results frommap
are "flattened" - placing their items directly into the result listfansh> ["abc", "def"].map { chars } [[97, 98, 99], [100, 101, 102]] fansh> ["abc", "def"].flatMap { chars } [97, 98, 99, 100, 101, 102]
-
mapNotNull
is the same asmap
except that null results are excludedfansh> tester := |Int x -> Int?| { if (x.isOdd) return x; else return null; } |sys::Int->sys::Int?| fansh> [1,2,3,4,5].map(tester) [1, null, 3, null, 5] fansh> [1,2,3,4,5].mapNotNull(tester) [1, 3, 5]
Two methods destructively rearrange the items in a list: sort
and sortr
.
The latter sorts the list in reverse order. These methods take an optional
function to define the comparator:
fansh> ["badger", "cat", "hare", "elephant"].sort [badger, cat, elephant, hare] fansh> ["badger", "cat", "hare", "elephant"].sort |Str a, Str b->Int| { return a.size <=> b.size } [cat, hare, badger, elephant] fansh> ["badger", "cat", "hare", "elephant"].sortr [hare, elephant, cat, badger] fansh> ["badger", "cat", "hare", "elephant"].sortr |Str a, Str b->Int| { return a.size <=> b.size } [elephant, badger, hare, cat]
The Map is a collection of values, indexed by keys.
As with the List, there are various kinds of operations supported on maps which accept functions as one of their parameters. There is no "split" style method, but there are analogous examples of the other four kinds of operations:
Kind of Operation | Kind of Result | Methods on Map |
---|---|---|
collapse to value | return a single overall value from function results |
all any join reduce
|
iteration | applies function to all, or sublist of, values |
each
|
search | return a new value or list of list values based on function results |
eachWhile exclude find findAll
|
transform | return a new list of values based on function results |
map mapNotNull
|
These methods are essentially identical to those on lists, but use (value,key)
parameter pairs for their function in place of (value,index). Methods like
findAll
and map
now return maps, preserving the keys from the original map.
The following three examples illustrate the similarities and differences:
fansh> [1:"one",2:"two",3:"three"].each |Str value, Int key| { echo("$key => $value") } 1 => one 2 => two 3 => three fansh> [1:"one",2:"two",3:"three"].findAll |Str value, Int key->Bool| { key.isOdd } [1:one, 3:three] fansh> [1:"one",2:"two",3:"three"].map |Str value, Int key -> Int| { value.size } [1:3, 2:3, 3:5]
There are three methods specific to maps which take functions as one of their arguments. These are:
-
addList
- given a list of values, the values are added to the map using as key either themselves or the result of applying the optional function to each value. If a key already exists in the list, then the method fails.fansh> m := [1:"one",2:"two",3:"three"] [1:one, 2:two, 3:three] fansh> m.addList(["four", "eleven", "thirteen"]) |Str value->Int| { value.size } [1:one, 2:two, 3:three, 4:four, 6:eleven, 8:thirteen] fansh> m.addList(["ten"]) |Str value->Int| { value.size } sys::ArgErr: Key already mapped: 3 fan.sys.Map.add (Map.java:165) fan.sys.Map.addList (Map.java:247)
-
getOrAdd
- extendsget
so that any missing keys are added using the value generated by the given functionfansh> n := Str:Int[:] [:] fansh> n.getOrAdd("one") |Str key->Int| { key.size } 3 fansh> n.getOrAdd("four") |Str key->Int| { key.size } 4 fansh> n [four:4, one:3]
-
setList
- is the same asaddList
, except it overwrites any existing mapping.fansh> m := [1:"one",2:"two",3:"three"] [1:one, 2:two, 3:three] fansh> m.setList(["four", "eleven", "thirteen"]) |Str value->Int| { value.size } [1:one, 2:two, 3:three, 4:four, 6:eleven, 8:thirteen] fansh> m.setList(["ten"]) |Str value->Int| { value.size } [1:one, 2:two, 3:ten, 4:four, 6:eleven, 8:thirteen]
Finally, it's worth noting that maps can be decomposed into two lists by using
the keys
and vals
methods to get lists of the keys or values, respectively.
This means you can use some list methods along with a map, e.g. to find which
keys are associated with the same value:
fansh> bylength := ["one":3, "two":3, "three":5, "four":4, "five":4] [four:4, one:3, two:3, three:5, five:4] fansh> bylength.keys.groupBy { bylength[it] } [3:[one, two], 4:[four, five], 5:[three]]
As with the List, there are various kinds of operations supported on strings which accept functions as one of their parameters. However, these are now reduced to a small "core set" of methods:
Kind of Operation | Kind of Result | Methods on Map |
---|---|---|
collapse to value | return a single overall value from function results |
all any
|
iteration | applies function to all, or sublist of, values |
each eachr
|
search | return a new value or list of list values based on function results |
eachWhile
|
These methods all work analogously to those on lists, except that the function is given each (Int
) character and its
index in turn:
fansh> "aBcDeF".all { isUpper } false fansh> "aBcDeF".any { isUpper } true fansh> "aBcDeF".each |Int char, Int index| { echo("$char ${char.toChar} at $index") } 97 a at 0 66 B at 1 99 c at 2 68 D at 3 101 e at 4 70 F at 5
Strings can be converted to or from lists of chars, using chars
and Str.fromChars
, respectively.
These methods allow the use of useful list methods. For example, to keep only the upper case
letters in a string:
fansh> Str.fromChars("aBcDeF123gH".chars.findAll { isUpper }) BDFH
Functions are used throughout the standard library. A good example is in the
File
library, where two methods provide a basic iteration structure with
functions filling out the details.
-
eachLine
- passes each line in the file, as a string, to the given functionfansh> `projects/pclTextBox/fan/Distance.fan`.toFile.eachLine { echo(it) } ** A collection of algorithms for measuring the distance between two strings. ** ** These distance measures are based on the number of changes needed to make ** the sequence of characters in one string match that in the other. Different ** algorithms use different operations. ** class Distance {
(OUTPUT TRUNCATED!)
-
walk
- if the file is a directory, then recursively walks through every file in the directory, passing each file to the function. If the file is a file, then calls the function once only, with the file itself.fansh> `projects/pclTextBox/fan/`.toFile.walk { echo(it) } projects/pclTextBox/fan/ projects/pclTextBox/fan/Distance.fan projects/pclTextBox/fan/Format.fan projects/pclTextBox/fan/Ngrams.fan projects/pclTextBox/fan/Phonetic.fan projects/pclTextBox/fan/Stemmer.fan projects/pclTextBox/fan/Stopwords.fan
The range datatype supports each
, eachWhile
and map
.
fansh> (10..15).each { echo(it) } 10 11 12 13 14 15 fansh> (10..15).map { "Id: $it" } [Id: 10, Id: 11, Id: 12, Id: 13, Id: 14, Id: 15]