tl;dr
I propose the compiler would expose a “magical” function enumerate : Type a -> List a
(for lack of better syntax) that would enumerate non-infinite types into a list:
type MyType
= Foo
| Bar Baz
type Baz
= A
| B
| C
enumerate Baz
--> [A, B, C]
enumerate MyType
--> [Foo, Bar A, Bar B, Bar C]
Motivation
I’m regularly encountering situations where I have a custom type for which I need all its values to be processed (shown, aggregated, …). For example this thread.
Currently there is no way to enumerate such types, so I need to create the list manually. The danger of that is that when I extend the custom type, I forget to add a value to that manually created list.
With the enumerate
function I’d be able to do this and be absolutely sure that I can’t forget about adding a new value anywhere:
parser : Parser (Route -> a) a
parser =
enumerate Route
|> List.map routeToParser
|> Url.Parser.oneOf
routeToParser : Route -> Parser (Route -> a) a
routeToParser route =
case route of
NotFound -> notFound
Home -> home
Ways to create the list semi-safely
a) introduce a redundant definition that uses case ... of
to warn me that I need to add a new value
parser : Parser (Route -> a) a
parser =
Url.Parser.oneOf
[ notFound
, home
]
{-| This is only here to make sure I don't forget to add the new route's parser.
-}
parserFailsafe : Route -> String
parserFailsafe route =
case route of
NotFound ->
"yes I have added the new parser to `parser` above"
Home ->
"yes I have added the new parser to `parser` above"
b) create the list of values myself with a case ... of
and slightly convoluted logic
nextType : Route -> Maybe Route
nextType : route =
case route of
NotFound -> Just Home
Home -> Nothing
makeAllRoutes : Maybe Route -> List Route
makeAllRoutes mbroute =
case mbroute of
Nothing -> []
Just route -> route :: makeAllRoutes (nextType route)
allRoutes : List Routes
allRoutes = makeAllRoutes NotFound
I’ve come to realize this one doesn’t have any benefit over the first option and is slightly more complicated.
Concerns
Q: what about (effectively) infinite types, ie. Maybe Int
or this ?
type MyType
= Foo
| Bar Int
A: The compiler would simply not allow using enumerate
on that type. Would throw eg. a compiler error about infinite lists.
Q: What about parameterized types? eg. Maybe a
A: The only allowed usages would be with concrete types:
-
enumerate (Maybe Bool)
would be OK -
enumerate Maybe
wouldn’t -
enumerate (Maybe a)
wouldn’t
Q: There is no support in the compiler for such reflection! (a Type -> Value
function)
A: Yeah, this would probably be the first such thing. But everything this function would need seems to be already in place / at least theoretically knowable.
- Is the type infinite? We can check that using set membership: is any of the “participating” types
Int
,Float
,String
,Char
? (maybe I missed some?) - Is the result of the
enumerate
call actually used anywhere? Dead code elimination tells us that. - If the type is not infinite and the result is actually used, the compiler can generate a JS value with the actual list, potentially inline, potentially reusing it in multiple places.
Pros
- Type safety for when you need to enumerate all values of a custom type (this is fairly common). This would eliminate the need to make the list yourself and possibly forget to add a value when you expand the custom type.
Cons
- Slightly magical/non-explicit language feature
- Possibly hard to implement? I don’t really know.
- Doesn’t work on all types, but - well, there’s no way it possibly could in a strict language. But this limits the usefulness of the function and the number of usecases where this can be used.