Remove 1 record field

Is it possible to define a function with this type?

{ a | field : Int } -> a
1 Like

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.

4 Likes

(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.

1 Like

@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.

4 Likes

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.

1 Like

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.

2 Likes

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?

2 Likes

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.

6 Likes

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.

2 Likes

This is now off-topic, but fun. There is the Never type that can’t be instantiated.
See Basics - core 1.0.5

4 Likes

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.