Module organization in functional languages

src/
*/* -- Business logic objects and algorithms go in their
own module categories. These are self contained
and don't access State, because they're more
general than a single app.
PortTypes -- Types used by ports, including an "interface"
record type containing all the app's port
functions as values. In F# or purescript, I
should substitute object or module style
interfaces to foreign code, but I've been in
practice lazier than that, instead injecting
dependencies on foreign code farther down the
stack with higher level semantics.
Types -- Most frequent "agnostic" types used, such as
currency, http message types, domain objects
Flags -- Data consumed at app startup (elm flags, parsed
command line arguments).
Ports -- A module that contains the ports in elm.
Because port values are functions, they can
be passed to views and such in a record that
allows testing code to consume surrogates.
State/Types -- Types used by and to update global state
Bus -- Messages consumed by State.update to change
the application's global state. They can be
sent by ports and view code, and run down the
application's OutMessage bus. Can use
State/Types
Codecs -- A central place for json, xml, parsing code
State -- Application state available to all code
View/ -- Active UI code (update and init fns)
View/Html/ -- HTML and SVG composition, to separate it from
message handling.
View -- Main app container, CSS generation if needed
Main -- App main and initialization, combines ports,
flags, state and View.* functions to make an app.
TestFeature*-- A main module that tests a specific feature. It
doesn't use real ports unless they're being
tested in their own right, but instead has
features to simulate port and end user activity.
  • You always know where bus messages terminate when they originate from the extremities of the app. The single source of truth becomes the .state member of the app data structure, which can be freely changed to suit specific situations.
  • The business logic behavior of the app becomes relatively easy to test. State is just messages and data and isn’t tangled up in view specific stuff.
  • Bus messages allow decoupling of app behavior and views. The app State is totally unaware of how views use its data and any disconnection between message traffic to the UI and ideal transformations on the app State is handled in View.*.update functions that take local messages and translate this activity into bus messages.
  • View local state (such as names edited for renaming) is strictly separated from single source of truth state.
  • By keeping Types early in the tree and separate from code or values, we can have nice concrete type annotations in all values and functions without needing to move any code. We can state the types of injected functions clearly whenever a function needs to be injected to counter a potentially wrong dependency arrow.
  • By segregating Flags apart from types, we can have different main functions use different flags or use them in a different way without being a pain. The downstream code becomes more general if it uses a type called out in Types for initialization that main can pull or synthesize from Flags.
  • Having a separate ports module is a good idea, but it doesn’t gain power unless ports are used through a port interface record that can be simulated in other ways. When your app uses this kind of organization, all but the main module becomes port agnostic. Conceptually, this could be a thing in F# too, where I’ve written a lot of code, but since the composition of the app depends on an fsproj file, I haven’t bothered making all foreign code injectible to the same degree, instead opting to inject surrogate code at the project level. I haven’t written enough purescript or haskell need this kind of hot swappability yet, but it’s only a matter of time before I do.
  • It’s not necessary, but I find it useful at larger scales to have subviews separate out actual HTML or SVG composition so the view’s primary module can focus on the data it contains and how messages transform it. I define the message type in the View module next to where I’m sending the messages but the Model in the primary module next to where the data is transformed. I don’t think there’s any better or worse way of arranging these two really, but that’s what I do.

--

--

--

An old programmer learning new tricks.

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Promote programming and earn money

Simple Quiz Application Using React Hook and TailwindCSS

WebSockets and Node.js

How to automate Firebase app deployment with GitHub Actions

HTML TUTORIAL FOR BEGINNERS PART 5

Better Error Messages in TypeScript 4.2 — Smarter Type Alias Preservation

Coding diary day 3: Basic functions, and first parameter

How to Manage React State with useReducer() and Context API

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
art yerkes

art yerkes

An old programmer learning new tricks.

More from Medium

Fundamentals of functional programming

Clojure and VS Code beginner setup

Why Clojure?

Find a matching element in a list and move to first: A journey from a novice JavaScript to an…