previous post: Scala what’s wrong with you!
Several years ago, I think at Uberconf, I was listening to yet another talk that showed some list manipulations and called it functional programming. I also remember being very confused about how to use FP in real life. Seeing bunch of examples did not translate, for me, to being able to write end-to-end application using these concepts. I remember talking to my colleague that it would be really good to have a comprehensive example, not just isolated 5 code-liners.
About a year ago, I decided to just do it. I decided to work on a simple CRUD application and implement it in a fully functional way. The idea grew and became: Polyglot application made out of interchangeable front-ends and back-ends implemented using various languages and functional libraries. https://github.com/rpeszek/typesafe-web-polyglot. At this moment I have 3 back-ends (Yesod-Haskell, Servant-Haskell, Http4s-Scala) and 2 front-ends (Elm, Halogen-PureScript). I will write short blogs describing my goals behind each of these projects. This post describes my general goals.
Comprehensive, what do I mean?
These are relatively small apps (a bit over 1000 lines each) that still facilitate the amount of code reuse I would expect to see in a large application. There is quite a bit of similarity between domain objects that expose create-retrieve-update-delete interface, thus, I expect to have code reuse too. These apps do what typical web-apps do today: talk to database, do Ajax communication, deal with Json, etc.
This is just about CRUD so there is no business logic to speak of and things are rather dull, but I think it becomes clear how would adding business logic look like in that code.
What is still not implemented?
Security, better styling (completely ignored in my current PureScript app), more complex (has-many) relationships.
Fully functional way, what do I mean?
Referential transparency (same computation, same inputs == same results) - this ought to be the defining property of FP since it enables everything else. Effectful computations can still be be referentially transparent. All of my apps treat this aspect with due respect. For example, my Scala backend app does only one unsafe operation: creates in-memory H2 DB at startup.
(Side-note: sadly this aspect did not make the cut in design of Java 8 streams - I say streams are based on opinions, not programming).
Reasoning about code - I find that the use of type class polymorphism retains good correspondence to logic. Method signatures look like rotated Gentzen notation with clearly identifiable assumptions (type constraints) and conclusion (result type). Here are 2 examples:
PureScript:
PureScript:
type AppM eff = (Aff (ajax :: AJAX , appconf:: APPCONFIG, nav:: NAVIGATION | eff))
ui :: forall eff model.
Show model =>
EntityRoute model =>
EntityReadHTML model =>
EntityGET eff model =>
Proxy model -> H.Component HH.HTML Query Input Void (AppM eff)
Scala:
case class CrudHandler[K,D,E[_]](uri: String)(implicit evM: Monad[E]
evPersitAsTask: E ~> Task,
evConvertKey: IntId[K],
evPersist: PersistCrud[K,D,E],
evJsonK: Encoder[K],
evJsonD: Encoder[D],
evDecodeJsonD: Decoder[D]) { ... }
Even without knowing what these are or wanting to admit that logic is important in programming it is evident that having the information organized into logical assumptions and conclusions makes things clear. For example (PureScript), I know that I am only using GET REST effects (no PUT, DELETE, etc), I know that my code is NOT using == comparison or has no sorting logic...
Polymorphism and generalization - most of my examples try to be polymorphic in both domain objects (being CRUD-ed) and in effects used. For example, the same code works with database back-ends and STM persistence and, at the same time, works across domain objects. In OO world that approach would probably violate KISS. In FP generalization is essential tool because it limits solution space (parametricity) and makes it harder to write programs that compile and do not work.
(Some projects have ‘simple’ branch which is more ‘hard-coded’ and less polymorphic).
(Some projects have ‘simple’ branch which is more ‘hard-coded’ and less polymorphic).
Type safety - I am interested in type safe FP (hence the selection of language choices). All application provide type safety over effects. Unique types are used as much as possible (for example integer IDs used in URLs or as DB keys have unique types instead of using just Integer. Big advantage of using typed functional languages is type safety in deconstruction (pattern matching) often referred to as non-exhaustive pattern match error. This type safety feature comes for free (maybe except for Scala which needs sealed annotation) and my example apps, obviously, take advantage of it.
Just looking at the method signatures shown above, one has to a least entertain possibility that this amount of type information could imply stronger type checking.
Just looking at the method signatures shown above, one has to a least entertain possibility that this amount of type information could imply stronger type checking.
Composability - standard monadic and applicative approaches are used to achieve composability. I think real-world examples like these CRUD apps make monadic computations feel right at home and feel very readable.
Side-note: It was somewhat of a revelation for me when I finally realized (Eugenio Moggi∗original https://core.ac.uk/download/pdf/21173011.pdf) that sequential composability == monads. Basically, composability == category, composable computations == Kleisli. You want composable code you need monads. Maybe trivial, but sends shivers down my spine.
Side-note: It was somewhat of a revelation for me when I finally realized (Eugenio Moggi∗original https://core.ac.uk/download/pdf/21173011.pdf) that sequential composability == monads. Basically, composability == category, composable computations == Kleisli. You want composable code you need monads. Maybe trivial, but sends shivers down my spine.