Compare commits
2 Commits
master
...
elegant-fp
Author | SHA1 | Date |
---|---|---|
Yann Esposito (Yogsototh) | f033295ae8 | |
Yann Esposito (Yogsototh) | 7799d5f43f |
|
@ -0,0 +1,157 @@
|
|||
#+title: Elegant Functional Programming Application Architecture
|
||||
#+description: An elegant and working code architecture
|
||||
#+keywords: blog static
|
||||
#+author: Yann Esposito
|
||||
#+email: yann@esposito.host
|
||||
#+date: [2021-09-26 Sun]
|
||||
#+lang: en
|
||||
#+options: auto-id:t
|
||||
#+startup: showeverything
|
||||
|
||||
In this article I will expose you about how to architect your application
|
||||
using function programming paradigm.
|
||||
|
||||
It is not tied to any programming language.
|
||||
The principles in this article could probably be used by most programming
|
||||
languages.
|
||||
* Pre
|
||||
:PROPERTIES:
|
||||
:CUSTOM_ID: pre
|
||||
:END:
|
||||
|
||||
First note that many /functional programming languages/ are not functional.
|
||||
They all provide holes in order to trick the system to be faster.
|
||||
Also for internal use, some advanced feature might be provided.
|
||||
|
||||
1. Type-classes ; this is not something I consider belongs to functional programming.
|
||||
2. Clojure Protocols, ~defmulti~ ; are not something I would consider belongs
|
||||
to functional programming.
|
||||
|
||||
And many other languages features.
|
||||
Let be radical functional programmer.
|
||||
|
||||
Radical, means going back to the roots.
|
||||
|
||||
So here we go.
|
||||
Functional programming is about manipulating functions with the meaning of
|
||||
*mathematical functions* not what most programming language call functions.
|
||||
A "function" in C, Clojure, Python, Java, Javascript is *not* a function in
|
||||
the functional programming sense.
|
||||
OTOH in Haskell it is /often/ a real function, but not always.
|
||||
|
||||
So a mathematical function is something that takes a bunch of parameters
|
||||
and return a value (or not, we should allow partial functions unfortunately).
|
||||
And if you provide the same parameter twice the result will always be the same.
|
||||
Also, it should not have any side effect, for example, it should log, emit
|
||||
sounds, ... nothing.
|
||||
|
||||
Whoa whoa... Give me a minute here. Why would we want to ... do nothing at
|
||||
all?
|
||||
Isn't writing an application about doing things?
|
||||
Yes, it is.
|
||||
|
||||
Let just say, that, you generally, don't need more than plunging a
|
||||
pure function graph into a context where an interpreter will be able to use
|
||||
and this big pile of pure functions will be used to have side effect.
|
||||
A big ball of pure functions are reproducible, easy to test, easy to
|
||||
analyze and reason about.
|
||||
Easy to expose business logic out of technical detail to access data,
|
||||
etc...
|
||||
|
||||
So I will provide a system as radical, and simple as possible that will
|
||||
have a lot of good properties that advanced functional programming methods
|
||||
(like Monad, Monads transformers, Free Monads, Effect systems) are trying
|
||||
to achieve.
|
||||
|
||||
* The Architecture
|
||||
:PROPERTIES:
|
||||
:CUSTOM_ID: the-architecture
|
||||
:END:
|
||||
|
||||
The main architecture is based on the /Service/ paradigm.
|
||||
A service will be a sub-application living at runtime, it will depends on
|
||||
other services, and have an internal state.
|
||||
|
||||
So in the end your application should look like an acyclic graph of services.
|
||||
|
||||
Every service has an initialization phase, then a living phase, then a stop phase.
|
||||
Every service declare a set of methods to be used and every service will
|
||||
keep an internal implicit state.
|
||||
|
||||
Even if this look a lot like Object oriented programming.
|
||||
It is in fact a quite radical functional programming architecture.
|
||||
|
||||
|
||||
First you declare an /interface/ a bit like in Java. So a lot less powerful
|
||||
than type classes.
|
||||
Really, just a declaration that the symbol ~my-interface~ represents a set of
|
||||
values and functions with some properties (like the number of arguments,
|
||||
the types, etc...)
|
||||
|
||||
Once that is done, you will be able to provide different /instances/ for this /interface/.
|
||||
|
||||
This will be useful to have a production instance and one or many test
|
||||
instances that will be useful to provide reproducible tests.
|
||||
|
||||
Then you declare a service as depending on other services a bit like this
|
||||
(in Clojure syntax):
|
||||
|
||||
#+begin_src clojure
|
||||
(def Interface
|
||||
(function-1 [arg-1 arg-2] "a function"))
|
||||
|
||||
;; Main implementation of /my-service/
|
||||
(defservice my-service
|
||||
Interface
|
||||
[sub-service-1
|
||||
sub-service-2
|
||||
,,,
|
||||
sub-service-n]
|
||||
(init [initial-ctx]
|
||||
(let [service-state
|
||||
(core/init {:sub-service-1 sub-service-1 ,,,})]
|
||||
(into initial-ctx service-state)))
|
||||
|
||||
(function-1 [ctx arg-1 arg-2]
|
||||
(core/function-1 ctx arg-1 arg-2)))
|
||||
|
||||
;; A test implementation of /my-service/
|
||||
(defservice my-test-service
|
||||
Interface
|
||||
[sub-service-1
|
||||
sub-service-2
|
||||
,,,
|
||||
sub-service-n]
|
||||
(init [initial-ctx]
|
||||
(let [service-state
|
||||
(test/init {:sub-service-1 sub-service-1 ,,,})]
|
||||
(into initial-ctx service-state)))
|
||||
|
||||
(function-1 [ctx arg-1 arg-2]
|
||||
(test/function-1 ctx arg-1 arg-2)))
|
||||
#+end_src
|
||||
|
||||
If you were to do that with say, javascript
|
||||
|
||||
|
||||
#+begin_src clojure
|
||||
let myInterface = {"function-1": undefined};
|
||||
let myService =
|
||||
instanciateService (myInterface,
|
||||
[subService1,subService2],
|
||||
function()
|
||||
{return {"init": function(state) {state.subService1 = subService1;
|
||||
state.subService2 = subService2;
|
||||
state.someConfigValue = subService1.getConfig("port")
|
||||
return state;
|
||||
},
|
||||
"function-1": function( arg1 ){ myFunction1( getServiceState(), arg1) }}};
|
||||
#+end_src
|
||||
|
||||
Mainly we need a mechanism to check the returned instanciation really
|
||||
instanciate all the entries. Depending on the programming language, you
|
||||
could go more or less far away, add checks at compile time or runtime.
|
||||
|
||||
We need a mechanism that will initialize an internal state, and a function
|
||||
~getServiceState~ that will be used as first parameter of all real
|
||||
implementation of these functions.
|
Loading…
Reference in New Issue