Converting the array of values into the record

Hello! I am making a ledger app for myself, and am puzzled with finding the proper transformation after lexer and parser steps into the app logic. I provide the part of text / types which are relevant for my question. Below the introduction of the file format, the actual question is in the “Ledger” section.

Playground

Ledger file

setting start_date "2025-08-24"
setting default_currency RSD
setting rate EUR 116.7

Lexer result

type t =
  | Directive(string) // setting, default_currency
  | Number(float) // -20.00, 42
  | Currency(string) // RSD
  | Date(Date.t) // "2025-01-01"
  | Newline
  // ...others

// Lexer result. In the code, Date variant has proper Date.t as the argument.
let result = [
  Directive("setting"), Directive("start_date"), Date("2025-08-24"), Newline,
  Directive("setting"), Directive("default_currency"), Currency("RSD"), Newline,
  Directive("setting"), Directive("rate") ,Currency("EUR"), Number(116.7), Newline,
]

Parser result

@unboxed type currency = Currency(string)

type setting =
  | StartDate(Date.t)
  | DefaultCurrency(currency)
  | Rate(currency, float)

type t =
  | Setting(setting)
  // ...others

// Parser result.
let result = [
  Setting(StartDate("2025-08-24")),  
  Setting(DefaultCurrency(Currency("RSD"))),  
  Setting(Rate(Currency("EUR"), 117.6)),
]

Ledger

// `open` the module with currency type

type settings = {
  startDate: Date.t,
  untilDate: Date.t,
  defaultCurrency: currency,
  rates: Map.t<currency, float>,
}

type t = {
  settings: settings,
  // ...others
}

// How to implement?
let make: parserResult => t = lines => {()}

What is the best way to transform an array of settings into the record? I came up with some solutions, but each has it’s flaws:

  • Use the same record type, but with optional fields (type duplication)
  • Use temporary Map.t (introduction of the type which mirrors setting type)
  • Use Array.find (scans lines array for each setting)

And then after iterating the array check that all fields are presented with Option.getExn.

But both ways seem too cumbersome and explicit for me. I want to check that all options are provided in the ledger file. And I do not want to modify parser to be more than line-by-line mapper.

Is there a better way?

I would create default records and reduce the array:

let defaultSettings = {
  startDate: "2025-08-24",
  untilDate: "2025-08-25",
  defaultCurrency: Currency("EUR"),
  rates: Map.make(),
}

let defaultLedger = {
  settings: defaultSettings,
}

let make: array<ParserResult.t> => t = lines =>
  lines->Array.reduce(defaultLedger, (ledger, parserResult) => {
    let settings = switch parserResult {
    | Setting(DefaultCurrency(defaultCurrency)) => {...ledger.settings, defaultCurrency}
 // | others
    | _ => ledger.settings
    }

    {...ledger, settings}
  })

Playground example

You might want to make some fields optional instead:

type settings = {
  startDate?: string,
  untilDate?: string,
  defaultCurrency: currency,
  rates: Map.t<currency, float>,
}

Of course this comes at the cost of worse DX but you get the compiler to check if the dates are not just dummy ones.

Thanks! I didn’t highlight this properly in my post - I want to check that all options are provided in the ledger file. But this validation should be done after the parsing phase to separate concerns. So the default or optional fields aren’t in sync with the business logic, in a way.

Then you should probably reduce to option<t>:

let make: array<ParserResult.t> => option<t> = lines =>
  lines->Array.reduce(None, (ledger, parserResult) =>
    ledger->Option.map(ledger => {
      let settings = switch parserResult {
      | Setting(DefaultCurrency(defaultCurrency)) => {...ledger.settings, defaultCurrency}
      // | others
      | _ => ledger.settings
      }

      {...ledger, settings}
    })
  )
1 Like

if you want the validation to be in a subsequent stage, making all the record field optional would make sense then, wouldn’t it? You’d then have a “validated” ledger settings record that you could modify alongside your business logic evolves.

1 Like

The more I think about it, the more it makes sense!