Row types and modules

April 01, 2023

I was thinking about modules a few years ago. One of the directions was the intersection of modules with row types. I decided to write something down to keep the idea.

OCaml consists of two languages:

  • term language — to construct programs with terms and types;

  • module language — to create signatures, modules, and functors.

1ML merges these levels — modules become values. I noticed that modules behave in a similar way/correspond to records. Since the Service/Handle pattern in Haskell is about using a record to represent an interface and to have different implementations by filling the record’s fields, what if we use row types on module level? If row types might be seen as an alternative to records, then it might be possible to represent row types values on module level as an alternative to modules.

Term language Module language
Records ~> Modules
Row types ~> ?

I don’t want to imagine an artificial language for this purpose and will use Haskell with the Handle pattern, extending the previous post.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import Data.Row

import qualified WindProvider
import qualified TemperatureProvider

type Location = String
type Day = String
type WeatherData = String

type Methods = ("getWeatherData" .== (Location -> Day -> IO WeatherData))
  .+ WindProvider.Methods
  .+ TemperatureProvider.Methods

-- We can remove an item from the record
type WindAndTemperatureMethods = Methods .- "getWeatherData"

test1 :: Rec WindAndTemperatureMethods
      -> Rec (WindProvider.Methods .+ TemperatureProvider.Methods)
test1 = id

-- It's possible to override methods
type MethodsWithUpdatedWeatherData =
  ("getWeatherData" .== (Location -> Day -> IO Int)) .// Methods

test2 :: Rec MethodsWithUpdatedWeatherData
      -> Rec (
          ("getWeatherData" .== (Location -> Day -> IO Int))
          .+ WindProvider.Methods .+ TemperatureProvider.Methods
        )
test2 = id

-- And to remove them by taking a row difference
type MethodsWithoutWeatherData =
  Methods .\\ ("getWeatherData" .== (Location -> Day -> IO WeatherData))

test3 :: Rec MethodsWithoutWeatherData -> Rec WindAndTemperatureMethods
test3 = id

-- The minimum join of the two rows.
type JoinedMethods = Methods .\/ Methods

test4 :: Rec JoinedMethods -> Rec Methods
test4 = id

That’s similar to my previous approach with vinyl library — Methods is just a union of other methods with its own getWeatherData extension. The difference is that vinyl record supports only appending new elements while with rows it’s also possible to remove items, take a difference, do a minimum join, override rows, etc.

By using row types as signatures we get more flexible interfaces, more ways to express and combine them. But I don’t remember if I ever had a motivation for this in a real world.

If you have a comment or feedback you may say hi.