elm / compiler

Compiler for Elm, a functional language for reliable webapps.
https://elm-lang.org/
BSD 3-Clause "New" or "Revised" License
7.54k stars 664 forks source link

Unable to implement `a -> ExtendedRecord a` (aka: an argument for typeclasses or against extensible records) #1283

Closed isovector closed 8 years ago

isovector commented 8 years ago

Given:

type alias Named a = { a | name : String }

withName : a -> Named a
withName a = { a | name = "isovector" }

the compiler responds

The type annotation for `withName` does not match its definition.

13│ withName : a -> Named a
               ^^^^^^^^^^^^
The type annotation is saying:

    b -> { b | name : String }

But I am inferring that the definition has this type:

    { b | name : a } -> { b | name : String }

The compiler thinks I'm trying to update a record, but I'm trying to add a new field. Without having dedicated syntax for this, the type signature of withName should be enough for the compiler to infer what I mean.

Since Named a is defined in terms of a, I should be able to use an a : a to create a namedA : Named a, but as far as I can tell, there is no way to do this in a polymorphic way.

jvoigtlaender commented 8 years ago

That feature (adding a field) does not exist anymore.

jvoigtlaender commented 8 years ago

So, is this a feature request (to add that feature back in) or a request to get a different error message?

isovector commented 8 years ago

I'd suggest re-adding this feature, in that case.

My use case is attempting to implement a type-safe data-level witness of which functions a polymorphic type supports. Typeclasses would also provide the desired functionality, but they seem to be explicitly off the roadmap.

More specifically, I want to derive Ord a instances for user types, given a canonical ordering -- in order to use these types in a Dict, which has a constructor empty : Ord a -> Dict a v. This works fine.

Later I realized I'd want to iterate over all the possible values, leading to Enum a. Given a function Ord a -> Enum a -> b, it would be nice to compact these witnesses into a single record type which is the composition of Ord and Enum. Adding fields to extensible records would support this use case.

jvoigtlaender commented 8 years ago

In that case, you should probably change the title of this issue accordingly. Also, prepare for this being a hard sell. Evan mentions in the Elm 0.16 release notes why he removed that feature. If you want to argue for putting it back in, you will have to overcome those reasons. Or provide a compelling enough use case, I guess.

isovector commented 8 years ago

The documentation states

You can also define extensible records. This use has not come up much in practice so far, but it is pretty cool nonetheless.

Given the context of this bug, the fact that extensible records haven't been used in practice shouldn't be surprising. Without the ability to construct extensible records, the overwhelming majority of use cases are crossed out.

With that in mind, I can see two ways of proceeding; either to a) drop support for extensible records entirely, or b) reimplement adding fields to records, so that this type feature is constructable.


As far as I can tell, the argument against typeclasses is that "scrap your typeclasses" can accomplish the same thing using records in Elm. The above is a use-case in which SYTC can't be applied in Elm due to non-composability of extensible records, the only type-level feature which could be capable of performing the necessary lifting.

mgold commented 8 years ago

That documentation is out of date. If there wasn't such a backlog of issues, I'd suggest fixing it.

jvoigtlaender commented 8 years ago

@mgold, what would there be to fix about that documentation? All it says is still true. It doesn't mention adding fields in expression syntax, for example.

Also, "has not come up much in practice" is a different proposition from "haven't been used in practice" (@isovector's wording). There are use cases for extensible record types without the ability to add fields in expressions. The documentation explains the general nature of those use cases, and there is at least one package with a corresponding practical use: http://package.elm-lang.org/packages/jackfranklin/elm-statey/2.0.0/.

(I'm explicitly not making a judgement about whether @isovector's other use case is compelling enough to bring back expression-level field addition. But I am not convinced that "the overwhelming majority of use cases are crossed out" without that specific subfeature. That's a bold claim.)

mgold commented 8 years ago

Ah -- there's a subtle difference between "you can define extensible record types" and "you can write functions that take extensible records as arguments", which are both true in 0.16, and "you can extend existing record values", which is no longer true. So the docs should be made more precise.

isovector commented 8 years ago

Re: the overwhelming majority of use cases are crossed out: I mean that quite literally:

A vanishingly small number of types are primitives, with all other types being defined via induction. Types are nothing but constraints on programs, and don't exist outside of our conceptual model -- at some point they need to cash out into data. Without the ability to define the data corresponding to these types inductively, the overwhelming majority of them are off-limits to the programmer. What's the point of being able to construct types that you can't instantiate?

The frustrating part is that defining the induction is super easy -- comment 1 is a good example. Unfortunately, the current record syntax provides no way of telling elm about the induction. The result is an exponential-in-the-number-of-starting-types amount of work the programmer needs to do.

There is a glaring asymmetry here: getName : { a | name : String } -> String is a single function I need to write in order to destruct infinitely many types, but withName : a -> { a | name : String } is infinitely many functions I need to write to construct infintely many types.

I would not consider this friendly compiler behavior.

jvoigtlaender commented 8 years ago

I get what you want to do but can't. Still, convincingly explaining that there are infinitely many things one can't do without the subfeature in question is not a (for me) convincing argument that there are too few uses of the feature without that subfeature (so that the whole feature should be abandoned if the subfeature is not added). Nothing in your latest comment addresses this point.

But anyway, maybe it's just me. And I am not the one whom you need to convince either way, since I'm not the one who decided or decides about that feature and/or subfeature being in or out of the language.

isovector commented 8 years ago

In the interim, I would be happy with (and less vocal) a solution that let me use pairs of user types as the keys of a Dict k v in a type-safe way without needing to do tons of work on my part.

Conceptually, this is nothing more than type alias DictFn : k -> v, but building a large DictFn incrementally would have terrible performance compared to a real hash map.

jvoigtlaender commented 8 years ago

You are not alone in this. It seems almost everybody wants this. I do. The need is registered: https://github.com/elm-lang/elm-compiler/issues/1008.

jvoigtlaender commented 8 years ago

Though, by the way, Elm core's Dicts are not hash maps.

Warry commented 8 years ago

I talked about this during the 0.16 release: https://github.com/elm-lang/elm-compiler/issues/1088

jvoigtlaender commented 8 years ago

@Warry, despite the specific example from the first comment above, I think @isovector wants something more general than what that other issue was about.

evancz commented 8 years ago

I don't want to readd this feature. The specific case of "I want dicts to hold union types" is something lots of folks agree would be nice. I don't think it'll be in 0.17, but I think this is a pretty likely this will become possible.

I think it makes sense to continue discussion in this issue if you think there are valid use cases that we did not see in the 2+ years of having the feature, or that did not come up in the discussions explicitly searching for use cases before it was removed, or that for some reason have not been brought up here already. Once there is a specific and compelling example, perhaps it makes sense to open a new issue that focuses on it explicitly. That said, I try not to use the issue tracker for feature requests.

alexspurling commented 7 years ago

@isovector wrote a convincing example of a use case for adding fields to extensible records here: http://reasonablypolymorphic.com/blog/elm-is-wrong Is that enough to convince you, @evancz ?

evancz commented 7 years ago

I mistakenly thought that @Warry linked to https://github.com/elm-lang/elm-compiler/issues/985 which was the original discussion of removing this feature. "I do not want to readd this feature" was a messy way of saying that all that reasoning is still more persuasive to me. I wrote about it a bit more in the "simplified records" section of these release notes and also tested the performance theory, which was true. I should have done a better job explaining all the context around this choice. I'm sorry that OP felt I was unfair or rude.

I have seen that article. Elm cannot please everyone, so if folks are unhappy with how things are right now, there are a few things to think about:

These are things that I don't think come across well in text, but I tried to outline them more clearly in this talk and I hope people will watch to get a better feel for how things work in this community.

As I said earlier in this thread, "I try not to use the issue tracker for feature requests." Feedback like this is helpful when making design choices, and I have the feedback now. I understand the argument being made here, and I don't think it will be a productive to continue in this particular thread.

In any case, I appreciate the feedback, and I'm sorry Elm made this person so angry. My goal is to help folks create cool projects and have fun learning, and it sucks when it is not that way for folks.