Monoid to make a configuration dsl¶
If you have read Gabriels presentation, you might think, that I would be trying to create monoid for combining event streams. Unfortunately, I don't think I am hard-core enough to do that in Python.
On the other hand, writing a tiny config library sounds like a bit of harmless fun. So, we will need the mappend function again.
from functools import singledispatch
@singledispatch
def mappend(a, b):
raise Error("Not implemented for" + a)
This lets us define mconcat.
def mconcat(l):
acc = l[0]
for x in l[1:]:
acc = mappend(acc,x)
return acc
For the configuration, we would need:
- lists
- dictionaries
- functions to process things
@mappend.register(list)
def _(a,b):
return a + b
@mappend.register(dict)
def _(a,b):
return {**a, **b}
@mappend.register(mconcat.__class__)
def _(a,b):
def result(*x):
a_r= a(*x)
b_r=b(*x)
return mappend(a_r,b_r)
return result
Now we could create a generic function, i.e. askFor:
def askFor(name):
def getAnswer():
answer = input(name)
return {name: answer}
return getAnswer
askAll = mconcat([
askFor('name'),
askFor('age'),
askFor('email')
])
askAll()
What we could do now, instead of asking for input manually, we could pass in a config string and parse it. I will first create the config string, with a simple structure "key:value" on each line.
config = """
name:eve
age:16
dance:swing
"""
Now I can create a simple parsing function, where I can input the key, and it will return the value. Actually, it will return a parser that takes the string produces a dictionary with single KV pair, but thats almost the same, just more composable :)
And for now, if it doesn't find the key, it produces empty dictionary.
import re
def parseFor(name):
def getAnswer(config):
m = re.search('(?<='+name+').*', config)
if m == None:
return {}
else:
return {name: m.group(0)}
return getAnswer
parseAll = mconcat([
parseFor('name'),
parseFor('age'),
parseFor('email')
])
parseAll(config)
And because in our parsing functions we get the whole config, we could parse different things as well. For example we could get names of all of the keys.
def getKeys(config):
return {"keys": [x.strip() for x in re.split(":.*\n",config) if x.strip()!= ""]}
parseAll = mconcat([parseFor('name'),getKeys])
parseAll(config)
Better config object?¶
Another observation we can make, is that we can create an alternative interpretation of dicitonary mappend, where don't just over-write the latter, but we assume that values of the dictionary are monoids themselves. This means we can mappend them again :-)
@mappend.register(dict)
def _(a,b):
return {**a, **b,**{k:mappend(a[k],b[k]) for k in a if k in b}}
We could then have additional alternative implementation for our function mappend.
Our new mappend would return the result of the first function, that doesn't return none
.
@mappend.register(mconcat.__class__)
def _(a,b):
def result(*x):
a_r= a(*x)
if a_r!=None:
return a_r;
b_r=b(*x)
return b_r
return result
This way we can flip the building blocks for our little dsl.
def askFor(name):
def getAnswer(config):
answer = input(name)
return answer
return {name: getAnswer}
def parseFor(name):
def getAnswer(config):
m = re.search('(?<='+name+').*', config)
if m == None:
return None
return m.group(0)
return {name: getAnswer}
We could create one more
parseAll = mconcat([
parseFor('name'),
askFor('name'),
parseFor('age'),
askFor('age'),
parseFor('email'),
askFor('email')
])
parseAll
The drawback is, that now we have dictionary of functions, where previously we had just a singe funciton that got us our config. We an still implement that:
def getConfig(parser, config):
return {k:parser[k](config) for k in parser}
Way we implemented this (dictionary keys get mappended and the first return from a funcion wins) we should only be prompted for the email, and rest of this should be parsed from config.
getConfig(parseAll,config)
parseAll2 = mconcat([
mappend(parseFor,askFor)('name'),
parseFor('age'),
askFor('age'),
parseFor('email'),
askFor('email')
])
getConfig(parseAll2,config)
And now I have a litle dsl where:
- the building blocks are nice, and I can just
mconcat
them together -
if I want to add new building block, it just needs to
- accept the config-string
- return a KV dict
On the other hand, it has no error handling, it is kind-of inefficient, and in practce would be just a useless toy. But there ae more powerful abstractions that would help us with this. Next time, functors?