fuelen

fuelen

What is the idiomatic shape for extensible keyword opts?

I’m working on a small library (GitHub - fuelen/mold: A tiny, zero-dependency parsing library for external payloads · GitHub) where schemas are plain data. Type options live in the 2nd element of the tuple: {:string, min_length: 2, max_length: 50}.I’d like to officially support custom keys here, so companion libraries can extend these opts.

Here’s what the typespec for :string looks like today:

@type string_type() ::
  {:string,
   trim: boolean(),
   nilable: boolean(),
   default: default(),
   format: Regex.t(),
   min_length: non_neg_integer(),
   max_length: non_neg_integer(),
   in: Enumerable.t(),
   transform: transform(),
   validate: validate()}
  | :string

This renders well in docs. I didn’t even factor the common opts (nilable, default, transform, validate) into a shared type, because then I’d have to write:

{:string, [
  {:trim, boolean()} |
  {:format, Regex.t()} |
  {:min_length, non_neg_integer()} |
  {:max_length, non_neg_integer()} | shared_option()
]}

It renders not as nice as the inlined keyword typespec, but I’m fine with it if needed.

Technically, I can already write

{:string, min_length: 2, my_custom_opt_for_another_library: :something}

and the library swallows unknown options. But the typespec says you can’t add custom options.

Here are the options I’m considering:

Option 1. Open keyword

@type string_type() ::
        {:string,
         [
           {:trim, boolean()}
           | {:format, Regex.t()}
           | {:min_length, non_neg_integer()}
           | ...
           | {atom(), any()} # <-- custom option
         ]}
        | :string

In this case, the list of known opts reads more like a hint than a contract.
Probably, it would be interesting to have an ability to write something like

{custom_option_name :: atom(), any()} when custom_option_name not in [:trim, :format, :min_length, ...]

but it actually means I want a map with the syntax of keyword list :slight_smile:

Option 2. Explicit :ext namespace

{:string, min_length: 2, ext: [some_ext_opt: ...]}

Verbose, but core opts stay strictly typed. Everything inside :ext is keyword() and available to extensions. But feels a bit artificial, like a pattern grabbed from other programming languages where type system doesn’t allow anything else.

Option 3. Just keyword()

Give up on typing opts and simply document allowed keys in @typedoc.

I think the first option is a good mix between 2nd and 3rd.
What would you pick? Is there an idiomatic shape at all?

First 10 of 13 Posts Switch mode

lud

lud

I would use maps :slight_smile:

Otherwise yeah, treat the specs as hints, and add good documentation on top of it.

mudasobwa

mudasobwa

Creator of Cure

This sounds exactly like the problem the core team had with Inspect protocol, resolved by Inspect.Optsstruct. I wouldn’t reinvent a wheel. Mold.Opts or like sounds good.

@type t() :: %Mold.Opts{
  …,
  custom_options: keyword(),
  …
}
fuelen

fuelen OP

Maps aren’t that nice due to lack of syntactic sugar
{:string, min_length: 1} vs {:string, %{min_length: 1}}

I like this idea the most, especially when there is a warning in docs:

Typespecs may be phased out as the set-theoretic type effort moves forward.

If new set-theoretic types could express such types – good. Otherwise, we won’t lose anything. I think it’s good to design the API around DX and Elixir’s expressiveness, not around the limitations of the current typespec tooling. If typespec can’t express it cleanly then typespec, not the API, should give way.


c’mon, that’s our job :smiley:

so, that’s, basically, option 2 from my list. Having a struct is not a necessary thing here, as public API for inspect/2 still uses a keyword list.

mudasobwa

mudasobwa

Creator of Cure

Correct me if I’m wrong, but you asked about the idiomatic solution. I honestly don’t know what would be more idiomatic than the language core. It accepts keyword() for convenience, but it immediately raises when keys are not known.

That is detectable by the typing system, unlike map().

krasenyp

krasenyp

Man, I’d love if Jason also supported this but the options are opaque and you can’t easily thread metadata when encoding.

mudasobwa

mudasobwa

Creator of Cure

I am not sure I understand what you do mean.

Jason supports encoding of whatever, and the only opaque thing there is custom_options, which might be explicitly asked to form some expected shape.

I have implemented Jason.Encoder for my structs gazzillion times and it’s perfectly handling metadata and propagates it all the turtles down the encoding.

lud

lud

Don’t compromise correctness to save typing three characters, especially in the AI coding era. If a map is what you need then just use that.

derek-zhou

derek-zhou

Syntactic sugar is important; Developer ergonomics is what make Elixir popular.

Having said that, my opinion is to use keywords when the user will provide often 0, at most 3 options. The OP’s pattern is clearly beyond that.

woylie

woylie

If you want to stick with the keyword list, I’d write it like this:

@type string_type :: :string | {:string, string_type_opt}
@type string_type_opt :: {:trim, boolean} | {:nilable, boolean} | ... | {atom, term}

However, with an open keyword list like this, there’s a collision risk if someone picks an option key that is later added to the library. To prevent that, it’s better to add a dedicated key under which custom options can be added. I’m not sure what the most common name is in the Elixir ecosystem. I’ve seen both custom_options and extra. custom_options seems clearer, and it’s used by Inspect.Opts, as mentioned above.

@type string_type :: :string | {:string, string_type_opt}
@type string_type_opt :: {:trim, boolean} | {:nilable, boolean} | ... | {:custom_options, keyword}

And with shared options:

@type string_type :: :string | {:string, string_type_opt | shared_opt}
@type string_type_opt :: {:trim, boolean} | {:nilable, boolean} | ... | {:custom_options, keyword}
@type shared_opt :: {:something, boolean} | ...
Asd

Asd

@type string_type_option_default() ::
    {:trim, boolean()}
    | {:format, Regex.t()}
    | {:min_length, non_neg_integer()}

@type string_type(t) :: {:string, string_type_option_deafult() | t} | :string

@type string_type() :: {:string, string_type_option_deafult()} | :string

Where Next?

Trending in Questions Top

stjefim
Hello! Suppose you are building workflow (order / task / payment) processing system with the following requirements: Each workflow con...
New
jonnycharles
I’m in search of an Elixir library that offers PDF generation capabilities similar to Ruby’s Prawn. While there have been discussions abo...
New
spammy
I’m looking to build a personal workflow to quickly deploy web applications written in elixir/phoenix, for local consumption (ie not on t...
New
silverdr
Using Phoenix.LiveView.TagEngine as an EEx.Engine is deprecated! To compile HEEx, use Phoenix.LiveView.TagEngine.compile/2 instead. Sta...
New
dli
Before I dive in myself, did anyone successfully sprinkle Hologram into their existing LiveView app? Looking for hints regarding: Addi...
New
bottlenecked
Hi all, I wanted to ask how the community is dealing with post-release steps. Today we have Ecto migrations, which make sure that the db...
New
michallepicki
I am using Oban and occasionally, shortly after a deployment, a handful of jobs can fail because of dependency on other parts of the syst...
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New
type1fool
I just stumbled on a newly redesigned elixir-lang.org. :tada: It looks like @Software_Mansion did the work, and I think it is generally a...
New
akoutmos
@hugobarauna and I (Alex Koutmos) have been hard at work on writing a book on Nerves that takes you from simply blinking LEDs to building...
New

We're in Beta

About us Mission Statement