Aetherus

Aetherus

I want to define a type using typespec like

@type foo :: "bar" | "baz"

But the compiler shows the following error

unexpected expression in typespec: "bar"

I really don’t want to make it String.t or something alike which is not specific enough. How can I fix it?

Thanks in advance.

Showing Posts 1 to 10

ericmj

ericmj

Elixir Core Team

You can’t express literal strings in typespecs, you might want to use atoms instead which support this and are more appropriate.

Aetherus

Aetherus OP

I checked the documentation, and it seems there’s no way to use string literals in typespec, but why? Why lists and maps, even binaries are OK, but strings are forbidden?

ericmj

ericmj

Elixir Core Team

Because different types have different use cases. Reconsider instead why you have of string literals instead of using atoms which are built for this use case and are supported as literals in typespecs.

Aetherus

Aetherus OP

I choose strings because they are user inputs, and I don’t want to stuff my RAM with atoms then blow my RAM some day. Also, I don’t want to whitelist all allowed input and convert them to atoms.

ericmj

ericmj

Elixir Core Team

If it’s user input you have to use String.t since you don’t know what strings they will input. If you have validated that the user input is within your allowed set of accepted strings you can then convert to atoms without leaking atoms.

If you don’t want to do this you have to use String.t.

benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe

If you don’t want to whitelist the input, then a typespec of "foo" | "bar" is a fiction since the user can enter anything that they want.

nietaki

nietaki

For future alchemists:

The nicest workaround that comes to my mind is something like this:

@typedoc """
"foo" | "bar" | "baz"
"""
@type almost_enum :: String.t

Afterwards you can use almost_enum in your typespecs and the possible values will show up in both the docs and the code in an easy to find place.

That being said, it’s still best to stick to atoms in that case whenever practical.

thepeoplesbourgeois

thepeoplesbourgeois

I know I’m reviving an oollllllllld topic here, but this was the highest-ranked Google result that wasn’t documentation I’d already read through.

By my reckoning, it makes sense to wish typespecs could use string literals, because API integrations don’t trade in atoms. And, structured as they are, it is still much more ergonomic to keep the keys they send over the wire in the string formats they were received in, than it is to write bespoke modules with struct definitions that enumerate all of those same keys and provide functions to convert maps parsed from JSON into those structs and then work with the data being received. Yet, trust is still rickety enough with APIs that it is also highly undesirable, and indeed a potential source of atom declaration leaks, to take JSON received from an API and parse it with, e.g., Jason.decode data, keys: :atoms, like some kind of cowboy.*

And I know that @ericmj’s reply about atoms being “built for this use case” was to @Aetherus’s particular one-string-or-another problem, but I struggle to see the same utility to atoms in my use case, where I want to document the anticipated shape of data, which is optimistically well-structured, but subject to additions, revisions, and removals, as seen fit by its third party developer – whom I am unlikely to be able to persuade into reconsidering any such changes – of that integration. I’m only contemplating the typespec to give myself and my language servr hinting as to what keys I can expect to be able to work with while handling that data, so that I have quick access to that information when I would otherwise have to rummage through references to it in other parts of my code & through my curl history entries of when I’d received it.

Beyond that, it strikes me that, in a language where function signatures can be crafted to raise exceptions when their arguments fall outside of strictly pattern-matched, any such circumstance where a finite set of inputs constitute the only success-path arguments to a function (or where said inputs exhibit a specialized workflow in contrast to generic, but still valid, arguments) are worth representing as such inside of the corresponding type definitions and documentation. To wit, the type definition for a hypothetical IP4.address or Color.rgba function would benefit immensely from its signature being expressible as {0..255, 0..255, 0..255, 0..255} :: Ip4.Address.t | Color.Swatch.t than from it only being possible to sat that the function receives arguments of {non_neg_integer, non_neg_integer, non_neg_integer, non_neg_integer}… and forgive my saying, but I sincerely doubt atoms would carry more utility in that situation.

*with utmost respect to the incomparable Erlang HTTP server, cowboy

John-Goff

John-Goff

You can already express integer literals as well as ranges with typespecs, so your ip address example would work as you wrote it.

With respect to an external API, I would say that in this case the fact that dialyzer does not have string literals is probably pushing you towards a better design. If you just assume that your API response always has the same string keys then you could get into a situation where the API response changes and now some deep layer of your code is blowing up and it’s not immediately clear why. Instead a better pattern is to write a wrapper around this API, validate it has the keys that you expect it to, and then turn those string keys into atoms. That way if the response changes you have one central place that you can change and your internal code can keep relying on the fact that if it has a response the keys are always present.

Edit: I now see I skimmed over where you said this above approach is not ergonomic. I disagree, I think it feels less ergonomic until you are bitten in the ass by something you weren’t expecting to change, changing. Then you begin to appreciate keeping your external touch points of your system isolated into their own internal representations.

thepeoplesbourgeois

thepeoplesbourgeois

That’s a fair point about failing when incoming data doesn’t have the expected shape, but I don’t think an internally-defined struct really confers any bit-in-the-ass guards over, e.g., ~W[expected keys in_the_map] |> Enum.map(&Map.fetch!(map, &1)), %{"expected" expected, "keys" => keys, "in_the_map" => in_the_map} = map (which seems, in fact, more or less prerequisite to ferrying those values into an internally-defined struct), or the Map.take!/2 function you’ve just inspired me to write.

I’m skeptical that an internal struct inherently defines a design that is “better”; as far as I can tell, it more just seems to be the design that Elixir and Erlang chose.

Where Next? Top

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
dli
Before I dive in myself, did anyone successfully sprinkle Hologram into their existing LiveView app? Looking for hints regarding: Addi...
New
roeland
Kia ora, We have been using elixir-google-api to connect to Google Drive. However, with the updates to Tesla due to CVEs this is now bro...
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
rahultumpala
Hello, I have an Elixir backend that implements a custom protocol over TCP. I want to load test the backend and assess the performance o...
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
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge & Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
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

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews