Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
isovector opened this issue Feb 10, 2016 · 18 comments

Comments

@isovector
Copy link

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
Copy link
Contributor

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

@jvoigtlaender
Copy link
Contributor

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

@isovector
Copy link
Author

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
Copy link
Contributor

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
Copy link
Author

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.

@isovector isovector changed the title Unable to implement a -> ExtendedRecord a Unconstructiveness a -> ExtendedRecord a (an argument for typeclasses or against extensible records) Feb 11, 2016
@isovector isovector changed the title Unconstructiveness a -> ExtendedRecord a (an argument for typeclasses or against extensible records) Unable to implement a -> ExtendedRecord a (aka: an argument for typeclasses or against extensible records) Feb 11, 2016
@mgold
Copy link
Contributor

mgold commented Feb 11, 2016

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

@jvoigtlaender
Copy link
Contributor

@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
Copy link
Contributor

mgold commented Feb 11, 2016

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
Copy link
Author

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
Copy link
Contributor

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
Copy link
Author

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
Copy link
Contributor

You are not alone in this. It seems almost everybody wants this. I do. The need is registered: #1008.

@jvoigtlaender
Copy link
Contributor

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

@Warry
Copy link

Warry commented Feb 11, 2016

I talked about this during the 0.16 release: #1088

@jvoigtlaender
Copy link
Contributor

@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
Copy link
Member

evancz commented Feb 12, 2016

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
Copy link

@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
Copy link
Member

evancz commented Nov 10, 2016

I mistakenly thought that @Warry linked to #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:

  • A language is a multi-decade project, and it makes sense to be very cautious about what gets in. If the goal is to have type classes in Elm, I'd want to do it in a great way. Design it to be really nice to use, just like any other designed part of Elm. I don't think it makes sense to hack together features to get a bad version of type classes. As I said before, making comparable types more flexible is definitely important. It is being tracked in "more flexibility for comparable types" #1008. Making a language requires prioritization, so important things may end up taking longer than you would prefer, especially when there are workarounds. I try to prioritize based on what Elm users want and need, not on what Haskell folks think Elm needs after spending a few hours with it.
  • If you do not like how it is in Elm, there are a bunch of other languages that may appeal to you more. I strongly encourage you to try them out! If you like them, you can contribute to those projects in a friendly and productive way. Everyone can be happy! It sounds like GHCJS would be a better fit for this person.

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.

@elm elm locked and limited conversation to collaborators Nov 10, 2016
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants