I’m trying to understand how to use extensible records to do what I want. Here’s a quick example of the situation I’m in:
module Foo exposing (..)
type alias Foo =
{ name : String
}
type alias Selectable a =
{ a
| isSelected : Bool
}
isSelectableSelected : Selectable a -> Bool
isSelectableSelected selectable =
selectable.isSelected
getFooName : Foo -> String
getFooName foo =
foo.name
makeSelectableFoo : Selectable Foo
makeSelectableFoo =
{ name = "Bar"
, isSelected = True
}
getMyFooName =
let
selectableFoo =
makeSelectableFoo
in
getFooName selectableFoo
This does not compile, with the following error:
-- TYPE MISMATCH --------------------------------------------------- src/Foo.elm
The 1st argument to `getFooName` is not what I expect:
37| getFooName selectableFoo
^^^^^^^^^^^^^
This `selectableFoo` value is a:
Selectable Foo
But `getFooName` needs the 1st argument to be:
{ name : String }
Hint: Seems like a record field typo. Maybe isSelected should be name?
Hint: Can more type annotations be added? Type annotations always help me give
more specific messages, and I think they could help a lot in this case!
I’m trying to find a way to write getFooName in a way that allows me to use it with whichever extended type I choose (Selectable for instance). As far as this function is concerned, anything with name property should work out, because all it cares about is whether the argument has the structure of a Foo (which only has a name).
I understand I could make it work with making a Fooable type alias and having a bogus “base type”:
module Foo exposing (..)
type alias Bar =
{}
type alias Fooable a =
{ a
| name : String
}
-- ....
getFooName : Fooable a -> String
getFooName foo =
foo.name
makeSelectableFoo : Selectable (Fooable Bar)
makeSelectableFoo =
{ name = "Bar"
, isSelected = True
}
getMyFooName =
let
selectableFoo =
makeSelectableFoo
in
getFooName selectableFoo
This gives me the behaviour I want, but I’ve had to create a bogus empty record type and “Fooable” doesn’t make as much sense as Foo.
Thank you for your reply! I hadn’t thought about defining getFooName's argument as an extensible record itself. I’ll keep that in mind!
For my real use case however, the burden I was trying to avoid was redefining Foo's fields elsewhere - I should have been clearer about that in my original post.
In a situation where Foo has 10 fields, all of which are used in this hypothetical getFooName, say I’m writing 5 functions like getFooName, repeating these 10 fields definition 5 times. Now if I change or add a field to Foo, I have to make 5 changes. (While not as extreme, my real-world use case defines 3 fields, but is used in 9 places, some of which use it as Selectable, some of it not.)
On the other hand, in the example, if I added or changed a field in Selectable, there’s only 1 place I’d need to change a definition, since isSelectableSelected (and every other function I could have written that used Selectable) doesn’t redeclare those fields. I am trying to find a way to get the same level of practicality for a base type alias as with an extended record.
It doesn’t seem feasible this way, but I must admit I’m curious as to why the compiler doesn’t permit the passing of a “bigger record” to a function as long as it defines the required fields.
I’m not sure to understand, but I could have written:
getFooName : Fooable a -> String
getFooName foo =
foo.name
It would have been the same, without repeating the fields.
You can also make a type alias Foo = Fooable {}, and then use Foo, and all your hypothetical functions could use a Fooable a without repeating the fields:
module Foo exposing (..)
type alias Fooable a =
{ a
| name : String
, age : Int
}
type alias Foo =
Fooable {}
type alias Selectable a =
{ a
| isSelected : Bool
}
isSelectableSelected : Selectable a -> Bool
isSelectableSelected selectable =
selectable.isSelected
getFooName : Fooable a -> String
getFooName foo =
foo.name ++ String.fromInt foo.age
makeSelectableFoo : Selectable Foo
makeSelectableFoo =
{ name = "Bar"
, age = 42
, isSelected = True
}
getMyFooName =
getFooName makeSelectableFoo
in elm repl:
> import Foo
> Foo.getMyFooName
"Bar42" : String
Note: again, in this meaningless example, isSelectableSelected = .isSelected. It could use more fields though in a real use-case.
That is pretty nice and works well to avoid redefining the fields, thank you! The only downside that remains is the relation between Foo and Fooable. It won’t necessarily be intuitive for a newcomer to understand the difference between the two.
I suppose extensible records are not really made for this use case. For now on my real-life project, I’ve kept a record with isSelected defined in, without extensible records, as it’s much simpler. Only downside is, I have to sometimes provide bogus isSelected data just so I can pass it to some functions.
I’ll keep on coding and I might find a better way to solve the underlying problem
Yeah, Elm works usually better with flat models (without nested extensible records or nested fields).
Bogus data should be avoided. A simple way would be to use a Maybe Bool for the model selection status and for isSelected. It might be preferable though to use a custom type like:
type Selection
= Unselectable
| Unselected
| Selected