Is it possible to define a function with this type?
{ a | field : Int } -> a
Is it possible to define a function with this type?
{ a | field : Int } -> a
It’s not possible. You’d need to re-construct the record, which you can not do, because you don’t know it’s structure.
(0) @DullBananas has asked two questions. The first, I think is this: Can I make a copy of a record, but with one field removed. This question @akoppela has answered, and I mostly agree with that answer. However, see (5) below. (Disclaimer: I’m new to Elm. My answer might contain an error.)
(1) The second question is
(2) The elm repl
says that this can be done.
> type alias Goal a = { a | field : Int } -> a
>
> solution : Goal { field : Int }
| solution = identity
|
<function> : Goal { field : Int }
(3) But why do we get the following error from the compiler?
> puzzle : Goal { field : Int }
| puzzle rec = rec
|
elm: Map.!: given key is not an element in the map
CallStack (from HasCallStack):
error, called at libraries/containers/Data/Map/Internal.hs:610:17 in containers-0.5.11.0:Data.Map.Internal
I’ve raised a issue for this.
(4) Regarding the first question. There are two ways to create a record in Elm 0.19. The one is to use a literal, either directly or in a type alias
. The second is to use a field update literal on an existing record.
Therefore inspection of the Elm files in a given app with give all the possible field names and, putting value types to one side, record types. So perhaps the record re-construction might be possible.
(5) Perhaps @DullBananas can achieve their goal by using
type alias Solution a = { a | field : Maybe Int } -> a
I’m interested to know why @DullBananas wants to remove a field from a record.
@DullBananas No it’s not possible. This functionality was removed in Elm 0.16 (see news/compilers-as-assistants).
@jfine2358 The Elm compiler has had long standing bugs with duplicate fields in record types (notice Goal { field : Int }
duplicates field
as { field : Int, field : Int } -> { field : Int }
). solution
should not type check in Elm’s current type system. And certainly the runtime exception should not occur in puzzle
. So both solution
and puzzle
demonstrate bugs in Elm’s current compiler.
To expand on this, Elm used to support the following syntax:
{a - field}
which would have the type signature you wanted. The current syntax { a | field = val }
used to be record addition (i.e. a -> {a | field : Int }
) and the current update was written as { a | field <- val }
(i.e. {a | field : Int} -> {a | field : Int}
).
There were some cool usacases for this. You can sometimes achieve similar shenanigans with clever phantom type usage, but generally Elm is designed to discourage overly clever code like that.
I can see that
type alias A = { field : Int }
is useful, but I don’t see how
type alias B rec = { rec | field : Int }
adds anything that’s useful. I’d appreciate seeing an example.
In theory, you could compose records:
type alias Resource a = { a | data : String }
type alias Named a = { a | name : String }
type alias NamedResource = Named (Resource {})
and have functions that would work on all matching aliases:
namedToString : Named a -> String
namedToString = .name
processData : Resource a -> Resource a
processData r =
{ r | data = r.data ++ "++"}
testData : NamedResource
testData =
{ name = "Foo" , data = "Bar"}
processTest = processData testData
namedTest = namedToString testData
In practice, I think this did not turn out to be as helpful as it looked initially.
It’s not really useful for data modelling, but can help to generalise function arguments. Something like
fullName : { any | firstName : String, lastName : String } -> String
fullName { firstName, lastName } =
firstName ++ " " ++ lastName
This way you can use it with any record that has at least those two fields.
I don’t see that your example adds something that is useful. We can already do:
fullName : { firstName : String, lastName : String } -> String
fullName { firstName, lastName } =
firstName ++ " " ++ lastName
Yes but that doesn’t allow you to do
fullName
{ firstName = "Martin"
, lastName = "Janiczek"
, age = 27
}
or
fullName
{ firstName = "Martin"
, lastName = "Janiczek"
, position = FrontendEngineer
}
at the same time, which is what { any | firstName : String, lastName : String }
does allow you to do.
EDIT: I do have to agree though that this doesn’t come up very often. I have mostly used extensible records for signaling which Model
fields my various view
functions care about (using { a | tab : Tab }
instead of Model
for example), but lately I don’t do even that.
I reckon there are some nifty uses for extensible records in elm-css etc., for using “generic” attributes only in specific HTML elements.
EDIT 2: Here is a live Ellie example: https://ellie-app.com/cLsNLhP3jkRa1
Thank you for this @Janiczek. This gives another reason for the type { rec | a : Int, b : Int }
being part of the Elm language.
Here’s something that bothers me. I read docs/syntax, which for more explanation pointed me to docs/records. There’s no reference on that page to the update pipe symbol |
being used in a type definition.
Are there other documentation pages available, that cover this aspect of the syntax and semantics of Elm?
It was there on an earlier version of that page and removed recently - I am guessing to avoid encouraging its use in data modelling.
But yes, a little annoying since it is part of the syntax.
Maybe some other syntax you didn’t yet find out about:
I found extensible records useful when designing a package for a graphql-like selection api. Here’s an example from the code:
selection : Selection { a | id : Attribute String, name : Attribute String } School
selection =
Rest.map2 School
(Rest.field .id)
(Rest.field .name)
You may need more context to understand exactly what the code is doing but it should at least be clear from the types that it relies on extensible records.
Without extensible records, there would be less reuse. Arguably this reuse isn’t necessary but it certainly seems nice to have.
Also note that extensible records are necessary to type the automatic record accessor functions.
So when you make a record a = { foo = 3}
, you can do a.foo
or even .foo a
, to get 3
. However, when you also make a record b = { foo = "sd", bar = "bar" }
, you can still do b.foo
. This works, because the Elm compiler gives .foo
the type .foo : { a | foo : b } -> b
. If Elm couldn’t do extensible records, than you would get a compiler error when using 2 records with the same field names in the same scope.
Very good point @gampleman . Here’s confirmation via the elm repl
.
> .name
<function> : { b | name : a } -> a
> get_name rec = rec.name
<function> : { b | name : a } -> a
So we need the extensible record type to give a type to the field access function .name
. I’m happy to accept that as the definitive answer to my question, why do we need the type { rec | name : val }
.
Put another way, if we need field access functions, then we also need a type for field access functions. I don’t like the idea of a type that can’t be instantiated. That’s why in my first post to this topic I instantiated { a | field : Int } -> a
. I’d be worried if it wasn’t possible to do that.
This is now off-topic, but fun. There is the Never
type that can’t be instantiated.
See Basics - core 1.0.5
This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.