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.

In [1]:
from functools import singledispatch

@singledispatch
def mappend(a, b):
    raise Error("Not implemented for" + a)

This lets us define mconcat.

In [2]:
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
In [3]:
@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:

In [4]:
def askFor(name):
    def getAnswer():
        answer = input(name)
        return {name: answer}
    return getAnswer
In [5]:
askAll = mconcat([
        askFor('name'),
        askFor('age'),
        askFor('email')
    ])
In [6]:
askAll()
namea
agea
emaila
Out[6]:
{'age': 'a', 'email': 'a', 'name': 'a'}

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.

In [7]:
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.

In [8]:
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
In [9]:
parseAll = mconcat([
        parseFor('name'),
        parseFor('age'),
        parseFor('email')
    ])

parseAll(config)
Out[9]:
{'age': ':16', 'name': ':eve'}

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.

In [10]:
def getKeys(config):
    return {"keys": [x.strip() for x in re.split(":.*\n",config) if x.strip()!= ""]}
In [11]:
parseAll = mconcat([parseFor('name'),getKeys])

parseAll(config)
Out[11]:
{'keys': ['name', 'age', 'dance'], 'name': ':eve'}

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 :-)

In [12]:
@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.

In [13]:
@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.

In [14]:
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

In [15]:
parseAll = mconcat([
        parseFor('name'),
        askFor('name'),
        parseFor('age'),
        askFor('age'),
        parseFor('email'),
        askFor('email')
    ])

parseAll
Out[15]:
{'age': <function __main__._.<locals>.result>,
 'email': <function __main__._.<locals>.result>,
 'name': <function __main__._.<locals>.result>}

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:

In [16]:
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.

In [18]:
getConfig(parseAll,config)
emaila
Out[18]:
{'age': ':16', 'email': 'a', 'name': ':eve'}
In [19]:
parseAll2 = mconcat([
        mappend(parseFor,askFor)('name'),
        parseFor('age'),
        askFor('age'),
        parseFor('email'),
        askFor('email')
    ])

getConfig(parseAll2,config)
emaila
Out[19]:
{'age': ':16', 'email': 'a', 'name': ':eve'}

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?