1. Simple as the oposite of complex
Complexity in software is the root of all evil, and simplicity is the oposite of complexity. Simple is not the same as easy, because sometimes we make software complex just because it is easy (think of adding a library from which you need just a function, which then needs to be upgraded and it's incompatible with other libraries etc).
A complex sistem is like this, where is is very hard to figure out what is going on, thus it cannot be debugged, extended or changed:
and a simple one is the oposite:
2. Simplicity in software DATA and FUNCTIONS*
We use computers to compute (apply functions) some data we need (the final state of a system), given some initial data (initial state of the system). So if we drastically reduce what software does, we end up with just data and functions.Example:
Obviously this sounds overly simplistic, real code is more complex, more functions are needed.
function greed(name){
var a = ["hello ", name];
var b =capitalize_first_letter(a);
var c =concat(b);
return c;
}
or we could:
Which starts to look like a pipe, where you send the initial_state, and expect at the end the final state.
Now if we need to solve a real world problem, I guess we could solve it by having:
- lots of simple functions, that take as input one parameter and return one parameter
- because the have one parameter in and one parameter our they can be composed
- simple functions put together as a pipeline and can solve very complex problems in a very simple way
3. Functional composition
Now we could compose the two functions into just one:
4. Example: From complex to simple using functional composition
A few years ago, I made a practical example. I'll add it simplified here.
Requirement: in the json that we receive on a server, we need to have a key “measurement”, that is mandatory, cannot be null, needs to be a string and cannot be empty string, Then we also need to make sure the length of the string is between 3 and 8 characters, and cannot be some reserved words like “password” or “archived". So the code is like:
def validate_simplest_json(json):
errors = []
if not json.has_key("measurement"):
errors.append("measurement cannot be missing")
else:
if json["measurement"]==None:
errors.append("measurement cannot be null")
else:
if not isinstance(json["measurement"], str) and not isinstance(json["measurement"], unicode):
errors.append("measurement needs to string or unicode")
else:
lenm=len(json["measurement"].strip())
if lenm==0:
errors.append("measurement cannot be an empty string")
else:
if lenm<3: data-blogger-escaped-div="">
errors.append("measurement needs at least 3 characters")
elif lenm>10:
errors.append("measurement needs at most 10 characters")
elif json["measurement"].strip().lower() in ["archived","password"]:
errors.append("measurement has a value which is not allowed")
return errors
Removing complexity can mean, more linear code, and an initial state, and simple composable functions:
ValidationState = namedtuple("ValidationState","json key errors exit”)
then I will extract the actual validations in simple functions, like:
def validate_simplest_json_imperative_linear_with_state(json):
initial_state = ValidationState(json=json, key="measurement",errors=[], exit=False)
state = validate_key_exists(initial_state)
if not state.exit:
state = validate_not_null(state)
if not state.exit:
state = validate_string_or_unicode(state)
if not state.exit:
state = validate_not_empty_string(state)
if not state.exit:
state = validate_length(state, 3,10)
if not state.exit:
state = validate_not_in(state, ["archived","password"])
return state.errors
And the functions are like:
def validate_key_exists(state):
print validate_key_exists.__name__,state
if not key_exists(state.json,state.key):
return state._replace(errors = state.errors+["{0} cannot be missing".format(state.key)])._replace(exit=True)
return state
def validate_not_null(state):
print validate_not_null.__name__,state
if value_null(state.json,state.key):
return state._replace(errors = state.errors+["{0} cannot be null".format(state.key)])._replace(exit=True)
return state
...
The code looks is now a series of functions that run with the result of the previous function if the exit parameter is not set to True. So basically having 2 functions f,g they’ll be composed like:
initial_state = …
state = f(initial_state)
if not state.exit:
return g(state)
And putting this in a function:
def compose2(f, g):
def run(x):
result_f = f(x)
if not result_f.exit:
return g(result_f)
else:
return result_f
return run
#compose n functions
def compose(*functions):
return reduce(compose2, functions)
And now the final validation code:
def validate_simplest_functional_composition(json):
initial_state = ValidationState(json=json, key="measurement",errors=[], exit=False)
composed_function = compose(
validate_key_exists,
validate_not_null,
validate_string_or_unicode,
validate_not_empty_string,
create_validate_length(3, 10),
create_validate_not_in(["archived","password"]))
final_state = composed_function(initial_state)
return final_state.errors
It is much better. It basically says: having an initial start of the system, run all these functions (validators) and at the end get a final state. Code: http://runnable.com/VNMhoTKLSn9Tm0GI/fighting-complexity-through-functional-composition-for-python
And it is:
5. So where can I use this?
If you're a backend developer, you can use it on a server (python example):
@mod.route('/api/1/save/' , methods=['POST']) @pi_service() def generic_save(version=1, typ=None):composed_func = compose_list([ can_write("tags"), change("json", request.json), change("session", get_session()), change("type", get_pi_type(typ)), change("object", None), change("transformer", get_pi_transformer(typ)), get_database_object, transform_from_json, save_database_object, index_tag_or_tag_group, pi_transform_to_json, ])return composed_func({})
or in a PDF generating server, written in Clojure over Apache Batik (using transducers but that's another discussion)
You could use javascript promises for piping, with React, if you're a front-end developer. The state of the system the model (immutable) and rendering is done views.render:
StoryboardController.prototype.move_point_by = function(page_object, point_index, dx, dy) { pi.startWith(model,"MOVE POINT BY") .then(function move_point_by(state){ pi.info("move point by", page_object, point_index, dx, dy); var cursor = get_selected_layer_cursor(state) + ".children" + find_cursor_pageobject(page_object, state); if (cursor) { var point_cursor = cursor+".points["+point_index+"]"; var point = pi.pi_value(state, point_cursor); var changes = {}; var nx=point.x+dx; var ny=point.y+dy; changes[point_cursor+".x"]=nx; changes[point_cursor+".y"]=ny; state = pi.pi_change_multi(state, changes); return resize_shape(state, cursor); } return state; }) .then(views.render) .then(swap_model) .then(REST.try_save_page) }
or
or in Clojurescript, where the state of the system is an atom (model) and every time it changes, the view is rerendered:
6. Conclusion
Using this model, code is easier to understand, debug, change, extend. Why:
- all the data is in a place
initial_state = ValidationState(json=json, key="measurement",errors=[], exit=False)
- functions are simple
def validate_key_exists(state):
print validate_key_exists.__name__,state
if not key_exists(state.json,state.key):
return state._replace(errors = state.errors+["{0} cannot be missing".format(state.key)])._replace(exit=True)
return state
- intermediary states can be easily debugged
composed_function = compose(
validate_key_exists,
validate_not_null,
debug,
validate_string_or_unicode,
validate_not_empty_string,
create_validate_length(3, 10),
create_validate_not_in(["archived","password"]))
final_state = composed_function(initial_state)
def debug(state):
print state.json, state.key, state.errors, state.exit
return state
- data changes flow in a single direction
In part two: robustness, we'll see how we could also make the code robust, by making the code run transactionally same as databases: either all runs or none and the state gets reverted to the previous one.
No comments:
Post a Comment