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 ![]()
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?
Trending in Questions
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #elixirconf-us
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #metaprogramming
- #hex
- #performance










First 10 of 13 Posts
lud
I would use maps
Otherwise yeah, treat the specs as hints, and add good documentation on top of it.
mudasobwa
This sounds exactly like the problem the core team had with
Inspectprotocol, resolved byInspect.Optsstruct. I wouldn’t reinvent a wheel.Mold.Optsor like sounds good.fuelen
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:
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
so, that’s, basically, option 2 from my list. Having a struct is not a necessary thing here, as public API for
inspect/2still uses a keyword list.mudasobwa
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
Man, I’d love if Jason also supported this but the options are opaque and you can’t easily thread metadata when encoding.
mudasobwa
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.Encoderfor my structs gazzillion times and it’s perfectly handling metadata and propagates it all the turtles down the encoding.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
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
If you want to stick with the keyword list, I’d write it like this:
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_optionsandextra.custom_optionsseems clearer, and it’s used byInspect.Opts, as mentioned above.And with shared options:
Asd