Professional Resume Template for Software Developers
List-based Monadic Computations for Dynamically Typed Languages (Python version)
1. List-based Monadic Computations for
Dynamically Typed Languages
Wim Vanderbauwhede
School of Computing Science, University of Glasgow, UK
2. Overview
Program Composition in Dynamically
Typed Languages
Objects
Monads
Defining Karma
Karmic Types
Combining Functions using Functions
The Rules
Designing Karma
Syntactic Sugar
fold and chain
Karma Needs Dynamic Typing
Karmic Computations By Example
Maybe
State
Parsing
Conclusion
4. Program Composition
Most Dynamic Languages use an OOP approach to
structure/compose programs
But what if you don’t like object orientation?
Or what if it is simply not the best approach?
An alternative approach to program composition:
Functional Languages use composition of higher-order functions
In particular, Haskell uses monads ...
6. Monads
a reputation of being esoteric
hard to understand
the province of academic
languages
(unknown poet, 21st century)
Monas Hieroglyphica,
(“The Hieroglyphic Monad”),
John Dee, Antwerp 1564
7. Monads in Dynamic Languages
Have been done many times for many languages
https://en.wikipedia.org/wiki/Monad(functional_programming)#Monads_in_other_languages
But have not found widespread adoption
So ...
8. The Claim
... are monads of practical use in day-to-day programming?
Yes!
Only we won’t call them monads ...
9. Karmic Computations
Karma is a Sanskrit word meaning action, work or deed.
A different approach from the many existing implementations of
monads in dynamic languages.
Based on types, lambda functions and higher-order functions.
10. Types and Notation
Haskell-style syntax for types:
A function g from a type a to a type b:
g :: a → b
A function s with two arguments and the result of type a:
s :: a → a → a
A function f from a type a to a type m b, i.e. a type m parametrised on
a type b:
f :: a → m b
A function h which takes as arguments two functions of type
a → b and b → c and returns a function of type a → m b:
h :: (a → b) → (b → c) → (a → m b)
11. Lambda Functions
Also knows as anonymous subroutines, i.e. functions without a
name.
They are expressions that can be assigned to variables.
Language Lambda Syntax
Haskell x y -> f x y
LiveScript (x,y) -> f x,y
Perl sub ($x,$y) { f $x,$y }
Python lambda x,y : f(x,y)
Ruby lambda { |x,y| f(x,y) }
12. Higher-Order Functions
Functions that take other
functions as arguments and/or
return functions as results
Available in your favourite
dynamically type language:
Perl, Ruby, Python, LiveScript,
JavaScript, ...
13. What is Karma?
A karmic computation consists of:
a given type m a;
the higher-order functions bind and wrap
and three rules governing those functions.
14. Karmic Types
A karmic type can be viewed as a “wrapper” around another type.
It does not have to have a particular structure, e.g. it can be an
object, or a list, or a map, or a function.
The key point is that the “karmic” type contains more information
than the “bare” type.
We will write m a as the karmic type constructor, regardless of the
structure of the type.
For example, in Python the following function f has a type
f :: a → m b, if g has type g :: a → b:
def f(x):
return m( g(x) )
15. Combining Functions
using Functions
The purpose of the karmic computation is to combine functions that
operates on karmic types.
The bind function composes values of type m a with functions
from a to m b:
bind :: m a → (a → m b) → m b
The wrap function takes a given type a and returns m a:
wrap :: a → m a
With these two functions we can create expressions that consists
of combinations of functions.
16. The Rules
Three rules govern bind and wrap to ensure that the composition is
well-behaved:
1. bind (wrap x) f ≡ f x
2. bind m wrap ≡ m
3. bind (bind m f) g ≡ bind m (x -> bind (f x) g)
In Python syntax:
1. bind(wrap(x),f) = f(x)
2. bind(m,wrap) = m
3. bind(bind(m,f),g) = bind(m,lambda x: bind(f(x),g))
17. Syntactic Sugar for Monads
In Haskell:
No sugar:
ex x =
x -> (bind (g x) (y -> (f (bind y (z -> wrap z)))))
Operator sugar:
ex x =
g x >>= y ->
f y >>= z ->
wrap z
Do-notation sugar:
ex x = do
y <- g x
z <- f y
wrap z
18. Syntactic Sugar for Karma
Our approach:
Use lists to combine computations
For example:
[
word,
maybe parens choice
natural,
[
symbol ’kind’,
symbol ’=’,
natural
]
]
19. Designing Karma
Grouping items in lists provides a natural sequencing
mechanism, and allows easy nesting.
In most dynamic languages, lists us the [...,...] syntax, it’s portable
and non-obtrusive.
We introduce a chain function to chain computations together.
Pass chain a list of functions and it will combine these functions
into a single expression.
20. Preliminary: fold
To define chain in terms of bind and wrap, we need the left fold
function (foldl).
foldl performs a left-to-right reduction of a list via iterative
application of a function to an initial value:
foldl :: (a → b → a) → a → [b] → [a]
A possible implementation e.g. in Python would be
def foldl (f, acc, lst):
for elt in lst:
acc=f(acc,elt)
return acc
21. Defining chain
We can now define chain as
def chain(fs):
lambda x: foldl( bind, wrap(x), fs)
Or in Haskell syntax
chain fs = x -> foldl (>>=) (wrap x) fs
22. Karma Needs Dynamic Typing
The above definition of chain is valid in Haskell, but only if the
type signature is
chain :: [(a → m a)] → (a → m a)
But the type of bind is bind :: m a → (a → m b) → m b
the karmic type m b returned by bind does not have to be the same as
the karmic type argument m a.
every function in the list passed to chain should be allowed to have a
different type!
This is not allowed in a statically typed language like Haskell, so
only the restricted case of functions of type a → m a can be used.
However, in a dynamically typed language there is no such
restriction, so we can generalise the type signature:
chain :: [(ai−1 → m ai )] → (a0 → m aN ) ∀i ∈ 1 .. N
24. Combining Fallible Computations (1)
Expressing failure
A Maybe class
class Maybe:
def __init__(self,value,status):
self.val=value
self.ok=status # true or false
For convenience: a fail instance
fail = Maybe(nil,False)
Assume f, g, h :: a → Maybe b
25. Combining Fallible Computations (2)
Without karma
v1 = f(x)
if (v1.ok):
v2=g(v1.val)
if (v2.ok):
v3 = h(v2.val)
if (v3.ok):
v3.val
else:
fail
else:
fail
else:
fail
With karma:
comp =
chain [ f,g,h ]
v1=comp(x)
26. bind and wrap for Maybe
For the Maybe karmic type, bind is defined as:
def bind(x,f):
if x.ok:
f(x)
else:
fail
and wrap as:
def wrap(x):
Maybe(x,True)
27. Stateful Computation
Using karma to perform stateful computations without actual
mutable state.
Example in LiveScript, because LiveScript has the option to force
immutable variables.
There is a growing interest in the use of immutable state in other
dynamic languages, in particular for web programming, because
immutable state greatly simplifies concurrent programming and
improves testability of code.
28. State Concept and API
We simulate state by combining functions that transform the
state, and applying the resulting function to an initial state
(approach borrowed from Haskell’s Control.State).
A small API to allow the functions in the list to access a shared
state:
get = -> ( (s) -> [s, s] )
put = (s) -> ( -> [[],s])
The get function reads the state, the put function writes an updated
state. Both return lambda functions.
A top-level function to apply the stateful computation to an initial
state:
res = evalState comp, init
where
evalState = (comp,init) -> comp!(init)
29. Trivial Interpreter:
Instructions
We want to interpret a list of instructions of type
type Instr = (String, ([Int] → Int, [a]))
For example
instrs = [
["v1",[set,[1]]],
["v2",[add,["v1",2]]],
["v2",[mul,["v2","v2"]]],
["v1",[add,["v1","v2"]]],
["v3",[mul,["v1","v2"]]]
]
where add, mul and set are functions of type [Int] → Int
The interpreter should return the final values for all variables.
The interpreter will need a context for the variables, we use a
simple map { String : Int } to store the values for each variable. This
constitutes the state of the computation.
30. Trivial Interpreter:
the Karmic Function
We write a function interpret_instr_st :: Instr → [[a], Ctxt]:
interpret_instr_st = (instr) ->
chain( [
get,
(ctxt) ->
[v,res] = interpret_instr(instr, ctxt)
put (insert v, res, ctxt)
] )
interpret_instr_st
gets the context from the state,
uses it to compute the instruction,
updates the context using the insert function and
puts the context back in the state.
31. Trivial Interpreter:
the Stateless Function
Using Ctxt as the type of the context, the type of interpret_instr is
interpret_instr :: Instr → Ctxt → (String, Int) and the
implementation is straightforward:
interpret_instr = (instr, ctxt) ->
[v,rhs] = instr # unpacking
[op,args] = rhs # unpacking
vals = map (‘get_val‘ ctxt), args
[v, op vals]
get_val looks up the value of a variable in the context; it is
backticked into an operator here to allow partial application.
32. bind and wrap for State
For the State karmic type, bind is defined as:
bind = (f,g) ->
(st) ->
[x,st_] = f st
(g x) st_
and wrap as
wrap = (x) -> ( (s) -> [x,s] )
33. modify: Modifying the State
The pattern of calling get to get the state, then computing using
the state and then updating the state using put is very common.
So we combine it in a function called modify:
modify = (f) ->
chain( [
get,
(s) -> put( f(s))
] )
Using modify we can rewrite interpret_instr_st as
interpret_instr_st = (instr) ->
modify (ctxt) ->
[v,res] = interpret_instr(instr, ctxt)
insert v, res, ctxt
34. mapM: a Dynamic chain
We could in principle use chain to write a list of computations on
each instruction:
chain [
interpret_instr_st instrs[0],
interpret_instr_st instrs[1],
interpret_instr_st instrs[2],
...
]
but clearly this is not practical as it is entirely static.
Instead, we use a monadic variant of the map function, mapM:
mapM :: Monad m ⇒ (a → m b) → [a] → m [b]
mapM applies the computations to the list of instructions (using
map) and then folds the resulting list using bind.
With mapM, we can simply write :
res = evalState (mapM interpret_instr_st, instrs), {}
36. Parser Combinators
Or more precisely, I needed to
parse Fortran for source
transformations.
So I needed a Fortran parser ...
... in Perl (for reasons).
So I created a Perl version of
Haskell’s Parsec parser
combinator library ...
... using Karma.
37. Parser Combinator Basics
Each basic parser returns a function which operates on a string
and returns a result.
Parser :: String → Result
The result is a tuple containing the status, the remaining string and
the substring(s) matched by the parser.
type Result = (Status, String, [Match])
To create a complete parser, the basic parsers are combined using
combinators.
A combinator takes a list of parsers and returns a parsing function
similar to the basic parsers:
combinator :: [Parser] → Parser
38. Karmic Parser Combinators
For example, a parser for a Fortran-95 type declaration:
parser = chain [
identifier,
maybe parens choice
natural,
[
symbol ’kind’,
symbol ’=’,
natural
]
];
In dynamic languages the actual chain combinator can be implicit,
i.e. a bare list will result in calling of chain on the list.
For statically typed languages this is of course not possible as all
elements in a list must have the same type.
39. bind and wrap for Parser
For the Parser karmic type, bind is defined as (Python):
def bind(t, p):
(st_,r_,ms_) = t
if st_ :
(st,r,ms) = p(r_)
if st:
[1,r,ms_+ms]
else:
[0,r,None]
else:
[0,r_,None]
and wrap as:
def wrap(str):
[1,str,None]
40. bind and wrap for Parser
For the Parser karmic type, bind is defined as (LiveScript):
bind = (t, p) ->
[st_,r_,ms_] = t
if st_ then
[st,r,ms] = p r_
if st then
[1,r,ms_ ++ ms]
else
[0,r,void]
else
[0,r_,void]
and wrap as:
wrap = (str) -> [1,str,void]
41. Final Example (1)
Parsers can combine several other parsers, and can be labeled:
f95_arg_decl_parser =
chain [
whiteSpace,
{TypeTup => type_parser},
maybe [
comma,
dim_parser
],
maybe [
comma,
intent_parser
],
symbol ’::’,
{Vars => sepBy ’,’,word}
]
42. Final Example (2)
We can run this parser e.g.
var_decl =
"real(8), dimension(0:ip,-1:jp+1,kp) :: u,v"
res =
run( f95_arg_decl_parser, var_decl )
This results in the parse tree:
{
’TypeTup’ => {
’Type’ => ’real’,
’Kind’ => ’8’
},
’Dim’ => [’0:ip’,’-1:jp+1’,’kp’],
’Vars’ => [’u’,’v’]
}
43. Parser Combinators Library
The Python version is on GitLab:
https://gitlab.com/wim_v12e/parser-combinators-py
The Perl and LiveScript versions are on GitHub:
https:
//github.com/wimvanderbauwhede/Perl-Parser-Combinators
https:
//github.com/wimvanderbauwhede/parser-combinators-ls
The Perl library is actually used in a Fortran source compiler
https://github.com/wimvanderbauwhede/RefactorF4Acc
44. Conclusion
We have presented a design for list-based monadic
computations, which we call karmic computations.
Karmic computations
are equivalent to monadic computations in statically typed languages
such as Haskell,
but rely essentially on dynamic typing through the use of
heterogeneous lists.
The proposed list-based syntax
is easy to use,
results in concise, elegant and very readable code, and
is largely language-independent.
The proposed approach can be applied very widely, especially as a
design technique for libraries.