Introduction
What is functional programming?, you may ask:
In computer science, functional programming is a programming paradigm where programs are constructed by applying and composing functions
Wikipedia’s definition of functional programming is OK, although purists would argue that functional programming includes pure functions that don’t modify state, and likely offer immutable data structures (as popularized by Clojure). However, Wikipedia’s definition is mostly good enough. In the past couple of decades the idea of lightweight functions, applied by library algorithms to provide a composable - and hopefully more declarative way of coding - has gained traction.
While it had been around before that, I was first introduced to functional programming concepts through C++‘s functional objects: create a small class that implements the ()
operator, maybe hold some data applied at call time, it gets called by some library algorithm at the appropriate time.
For more information on this pattern in C++, see “The Function Object Pattern”, from in C++ Report, Vol 9 #9, pages 32–42, October 1997.
Functional objects are great, and still a trick I sometimes use today: objects marry behavior with data, and functional objects were no different. Sometimes you need to seed some extra data into your lightweight function execution! This can be data in addition to whatever data you’re getting passed via an “for every element in this list, call this method passing as a parameter the current value” algorithm function.
Over the last decade I’ve seen functional paradigms become more and more accepted, and easier and easier to use as a developer. What once took you a whole C++ class and boilerplate is now a couple characters of syntax, for example.
In modern languages, almost every standard library leans into the idea of anonymous user provided functions (called lambdas or closures) for standard iteration and algorithm methods.
What if we look at functional programming concepts not through a modern language, but through the eyes of a 50 year old one? Smalltalk, first created in 1972ish, then widely released as Smalltalk-80.
Let’s first start by examining Smalltalk a bit, then jump into common functional paradigms in languages, from most frequently seen (in mainstream multi-paradigm languages) to least. (Yes, I know about Scala).
Smalltalk: a vibe shift
Smalltalk is one of the very first object oriented languages. It’s also super neat because it doesn’t have any built-in control structures!
record scratch
“Ummm, does that mean it can’t do if
statements?”
Gentle reader, here’s how you do an if statement in Smalltalk:
true ifTrue: [ 'I was true' ].
Here we have true
(an instance of the Smalltalk True
class, a subclass of Boolean
). We call the method ifTrue:
on it, and pass it a small, composable function. As the instance is true, our function is executed.
The False
class’s implementation simply does nothing, which is an elegant way to solve this bootstrap problem: by polymorphism, a sprinkling of inheritance, and a language where everything everything is an object.
Back to those small, composable, inline function things. Smalltalk calls these little functions “blocks” (I assume because the square brackets makes it look like your logic is inside a little rectangle), but other languages call them closures or lambdas. Theoretically there’s a difference, but practically everyone uses either one of these three names to mean the same kind of thing.
Functional iteration
Functional programming is most often used when iterating over a list: selecting items from a list, transforming items in a list into a different kind of object, etc.
Done simply in Smalltalk. First, set ourList
to a three array element, and we iterate on it
ourList := { 1. 2. 3 }.
ourList collect: [:in | in + 1 ].
This returns a list: 2, 3, 4.
Sometimes you want to take a list, grab only certain elements out of it, then operate on those. Ideally you want to do this in an efficient way: only operating on elements you’re going to use: if a data element matches the select it is immediately processed by the collect, vs waiting for the entire list to be filtered.
ourList select: [ :num | (num \\ 2) = 0 ] thenCollect: [:n | n + 2]
Two points:
- Yes, the method’s actual name is
select:thenCollect:
- Yes, the collect closure is only called for even numbers
\\
means mod / return the remainder
Smalltalk offers an OK collection of these complex methods. But maybe I have a situation where I want to select the even numbers, add two to them, then ONLY keep the result if it’s > 8.
Method Chaining
In this view of functional programming - the composing of small functions and moving data through a chain of operations - it should be easy to chain function calls together. The Lisp people often call these threads, or thread macros, which is slightly confusing because of CPU threading or when in languages, like Smalltalk, that don’t have macros.
Back to our example, what if we wanted to perform an additional select at the end of our select:thenCollect
workflow above? Let’s say select:thenCollect:andCollectSomeMore:
?
Using an excellent 10 line chaining solution by Attila Magyar we can get just that:
ourList chain
select: [ :num | (num \\ 2) = 0 ];
collect: [:n | n + 2];
collect: [:m | m + 2].
This is a wonderful, lightweight example of what Smalltalk can do, the expressiveness in almost zero syntax. (In Smalltalk ;
means “send this message to the same receiver”. Which in this case is doing some state management an proxying those messages to the result of the last expression)
There is in fact nothing magical about this, we could write a similar thing by hand:
((ourList select: [ :num | (num \\ 2) = 0 ]) collect: [:n | n + 2]) collect: [:m | m + 2]
This doesn’t feel good: counting the (
s was not fun, even in this trivial example.
It’s worth noting that, unlike the builtin select:theCollect:
method, chain
does not result in a lazy workflow. The entire collect is processed in a step, then the entire thing passed off to the next step.
Deep Selection / Traversal Systems
In complicated APIs or programs, sometimes that data we want is deep inside a structure full of stuff we don’t. Chris Penner calls how we access this data a traversal system, where as Sergio Antoy and Michael Hanus’ paper on new functional logic design patterns (site) would call it “deep selection” (then they present an implementation that can only be achieved by functional-logic languages, so they may disagree with my application of the term.)
Anyway, traversal systems allow a developer to get a single value(s) “over there, and deep inside”. The Haskell community created the idea of lenses on data structures, and this idea has been ported to other languages (I’ve played with the Racket implementation and a Javascript one too).
So what does Smalltalk give us in these regards?
For Dictionaries, not lists like ourList
, Smalltalk offers some object traversal methods.
First, we construct our objects to work with
deepDatadictInner := Dictionary newFrom: { 1 -> 2. 'myKey' -> 4. 5 -> 6 }.
outerDataDict := Dictionary newFrom: { 'outer' -> deepDatadictInner. 'inner' -> 'hi'}.
Smalltalk’s Dictionary
objets have an at:at:
method which let you select an element from the Dictionary, then call at on that. Much like the select:thenCollect:
method, you could duplicate this with some parans, but that’s ugly
outerDataDict at: 'outer' at: 'myKey'.
returns 6.
Thats one of the basic, builtin methods from Smalltalk. Of course, it has limits as theres only at:at:
, so for deeper objects you may need our chain
method, above.
When Deep Select gets you in a bind: or stealing concepts from Kotlin (scope methods)
The Kotlin standard library contains several functions whose sole purpose is to execute a block of code within the context of an object.
In Kotlin this is called scope methods
What does this really mean? If you’re in the middle of a functional workflow, but need to do something not exactly functional, you can execute a block of code, getting the message receiver passed in as a parameter. Then pass the result of that expression to the next item in the chain.
Smalltalk’s has the in:
method that does just that. We’ll see it here, without chain
:
((ourList collect: [:in | in + 1 ]) in: [:newList | newList compact. newList. ]) asArray.
compact
doesn’t return anything, so we use in:
to call it, then return our object again to keep the workflow working. (In Smalltalk, the value returned from a block is the result of the last statement in the block.)
(We could also get fancy and phrase the block like newList compact; yourself
, where yourself
is a message that returns it’s receiver / instance.)
We could write this without a functional workflow, it would just take more lines:
|newList|
newList := ourList collect: [:in | in + 1 ].
newList compact.
newList asArray.
One line is somewhat more elegant here.
Traversal systems in Smalltalk when doing work with JSON
In the modern world of APIs, developers will often be dealing with API responses in JSON, and in normal RESTful intefaces the desired information may be deeply buried in some structure (unlike GraphQL where the client controls the shape of the object returned).
Smalltalk has the NeoJSON library with one class for happy, fast and dirty hacking: NeoJSONObject
.
NeoJSONObject
lets you access keys in a JSN structure as if they were messages in Smalltalk.
json := NeoJSONObject readFromString: jsonStringVersionOfOuruterDataDict.
json outer myKey.
I really like traversal systems that are easy to write for easy cases. And if there’s a more complicated structure, perhaps use NeoJSONObject
as far as you can, then use more fine grained tools where you have to.
Pattern Matching
A more complex control statement in modern languages is the idea of a “pattern matching” control structure. Think of it as a very good switch
statement or an if with multiple conditions or results.
Kotlin calls this a when expression and it look like so:
when (x) {
0, 1 -> print("x == 0 or x == 1")
name == "ryan" -> print("even unrelated things do work")
else -> print("otherwise")
}
Only one of these expressions will evaluate to true, and once matched no other branches are matched.
Since Smalltalk doesn’t even have if
statements as syntax, can we add pattern matching to Smalltalk?
You betcha. I have, in rwilcox/PatternMatcher!
x patternMatchWithRules: {
PMRule whenMatches: [ :o | (o = 0) or: (o = 1) ] execute: [ 'x == 0 or x == 1' ]..
PMRule whenMatches: [ name = 'ryan' ] execute: [ 'hi, Ryan ].
PMRule whenMatches: [ true ] execute: [ 'else' ].
}
In those PMRule
lines, if the whenMatches
block returns true the execute
block is run. Else the next rule is evaluated.
Conclusion
Modern multi-paradigm languages allow for great functional patterns inside them. From languages almost nobody thinks of as functional (C++, Kotlin), to languages that are functional but impure (Racket), to pure functional languages (Haskell). These concepts can be cherry-picked into very unexpected places: the 50 year old very OO paradigm heavy Smalltalk. Smalltalk, which talks to providing almost nothing by the way of control structures lets bootstrapping and elegance happen by developers.
from Hacker News https://ift.tt/xThwR8s
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.