Compare commits

...

2 Commits

Author SHA1 Message Date
Yann Esposito (Yogsototh) f033295ae8
minimal progress 2021-10-04 10:50:49 +02:00
Yann Esposito (Yogsototh) 7799d5f43f
First draft 2021-10-04 10:08:41 +02:00
1 changed files with 157 additions and 0 deletions

View File

@ -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.