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

Higher kinded polymorphism #324

Open
rust-highfive opened this Issue Sep 25, 2014 · 96 comments

Comments

Projects
None yet
@rust-highfive
Copy link

rust-highfive commented Sep 25, 2014

Issue by tiffany352
Monday Sep 02, 2013 at 01:14 GMT

For earlier discussion, see rust-lang/rust#8922

This issue was labelled with: A-typesystem, I-wishlist in the Rust repository


Rust doesn't support higher kinded polymorphism, despite being a common feature in many functional programming languages. As far as I know, it is a commonly requested feature from people in the FP crowd.

@larroy

This comment has been minimized.

Copy link

larroy commented Dec 28, 2014

To fully support the idioms that HKP supports we would also need implicits, this would be also useful for passing the execution context to futures for example

@AlexanderKaraberov

This comment has been minimized.

Copy link

AlexanderKaraberov commented Jan 2, 2015

Can someone tell the status of higher kinded polymorphism in Rust? When will this feature be added to the language approximately?
I read related issue but nevertheless couldn't find any new information.

@Gankro

This comment has been minimized.

Copy link
Contributor

Gankro commented Jan 2, 2015

No one is currently implementing it, and no one has made a solid specification of what it would look like. If it happens, it will not be for a long time.

@huonw

This comment has been minimized.

Copy link
Member

huonw commented Jul 1, 2015

Search keywords: higher kinded types, HKT, monad, functor. (cc #1185 (comment))

@int-index

This comment has been minimized.

Copy link

int-index commented Aug 21, 2015

Why is no one working on this?

@antonkatz

This comment has been minimized.

Copy link

antonkatz commented Sep 11, 2015

Are there any status updates at this time?

@steveklabnik

This comment has been minimized.

Copy link
Member

steveklabnik commented Sep 11, 2015

@antonkatz not yet. Lots of work is going into implementing the MIR/HIR RFCs, which enable more typesystem features by making the internal representations easier to operate on.

@ticki

This comment has been minimized.

Copy link
Contributor

ticki commented Dec 7, 2015

@steveklabnik I wonder what it would look like. Do anyone have any ideas about how the semantics, design, and syntax would be?

@steveklabnik

This comment has been minimized.

Copy link
Member

steveklabnik commented Dec 7, 2015

Nobody has come up with that stuff yet, no, or at least, no serious proposals.

@tiziano88

This comment has been minimized.

Copy link

tiziano88 commented Dec 7, 2015

OK, let's talk about syntax as a light starter then.

Looking at the proposals made so far for Swift in typelift/swift#1, I personally do not find any of them particularly appealing or suitable for Rust.

Currently I would be leaning towards something like the following (using classic Functor/List example):

Trait declaration

trait<A> Functor {
    fn fmap<B>(&self, f: Fn(A) -> B) -> Self<B>;
}

Here Self is inferred to be of kind * -> *, based on the generic parameters to trait, therefore it always needs to have the appropriate number of generics when used in the function signature, so that the kind of the concrete types is always * (in this case, a return type of Self on its own would not be allowed). self is of type Self<A>.

Note that I think this means that this syntax can only express only one "layer" of extra type arguments, and could not easily express kinds higher than that (e.g. * -> * -> *).
Edit: That is incorrect, the syntax can easily support extra "flat layers", just by adding extra generic arguments to trait, e.g. trait<A,B,C> would make Self of kind * -> * -> * -> *.

Also, HKT would only exist in the context of a trait declaration (e.g. a function outside of a trait could not have a return type of kind * -> *, e.g. List); this restricts the scope of the proposal and simplifies the implementation, but we should consider tradeoffs and alternatives.

Trait implementation

impl<A> Functor for List<A> {
    fn fmap<B>(&self, f: Fn(A) -> B) -> List<B> { ... }
}
@glaebhoerl

This comment has been minimized.

Copy link
Contributor

glaebhoerl commented Dec 8, 2015

Cross-pasting from the duplicate issue I opened:

There is some previous discussion about HKTs, type lambdas, type inference, and so on in the comments on the default type parameters RFC.

Given that : at the type level is already taken for trait bounds, here is my basic idea for the syntax of kinds.

The discussion and referenced papers from this Scala issue may also be relevant for us.

@tiziano88

This comment has been minimized.

Copy link

tiziano88 commented Dec 8, 2015

@comex

This comment has been minimized.

Copy link

comex commented Dec 8, 2015

trait<A> Functor {

To me this looks confusingly similar to trait Functor<A>. (And what relationship does that A have with the one in the Fn?)

Alternate strawman: trait Functor : for<T> type

@int-index

This comment has been minimized.

Copy link

int-index commented Dec 8, 2015

Wadler's law in action...

@tiziano88

This comment has been minimized.

Copy link

tiziano88 commented Dec 8, 2015

@comex : yes that may be confusing at first, but I think it could make sense:

  • trait Foo { fn bar() -> Baz<A> }: A is a concrete type (this assumes that A is the actual name of a struct, for instance); Baz<A> is always a concrete type.
  • trait Foo { fn bar<A>() -> Baz<A> }: A is a generic type that has to be supplied (or inferred) when calling bar.
  • trait Foo<A> { fn bar() -> Baz<A> }: A is a generic type that has to be supplied (or inferred) when implementing Foo.
  • trait<A> Foo { fn bar() -> Baz<A> }: Foo has kind * -> *; A does not have to be provided in the trait impl; the trait impl abstracts over this type variable, and uses it to collapse the kind of the trait: Self has kind * -> *, Self<A> has kind *.

The pattern here is that, as the introduction of the generic type moves from right to left, the binding of that type parameter to a concrete type happens later and later.

@brendanzab

This comment has been minimized.

Copy link
Member

brendanzab commented Dec 8, 2015

Having a decent syntax for expressing higher kinded traits is important, but I am sure there are more pressing challenges that need to be solved when it comes to figuring out the intricacies of semantics, and how these interacts with the existing features of the type system. Do we need to wait for a more solid, formal foundation for the type system, perhaps post MIR? How does the feature interact with type inference?

How will libraries look once they can take advantage of higher kinded polymorphism? Can we have some interesting examples of what might be possible on the library front to help guide the design process?

@tiziano88

This comment has been minimized.

Copy link

tiziano88 commented Dec 8, 2015

@bjz yes I think we definitely need to wait for MIR before we can have a decent idea of how this may be implemented and how it interacts with the rest of the type system, that's why I suggested to start thinking about (or just playing around with) syntax for the time being. If you think this issue is not appropriate to discuss that, I am happy to move the discussion somewhere else.

Good point about providing examples of what would be possible with HKTs that is currently impossible or not ideal, it will be useful also to guide the discussion about semantics once we get closer to that.

@brendanzab

This comment has been minimized.

Copy link
Member

brendanzab commented Dec 8, 2015

Yeah, having a good base of concrete examples might help guide syntax, and figure out desirable semantics. At least it is something that can be done in the mean time, and would really help with eventually crafting high quality RFC.

@RalfJung

This comment has been minimized.

Copy link
Member

RalfJung commented Dec 8, 2015

A fairly simply, but IMHO interesting example is improving the API for iterators: With HKT, we could define the Item associated type to be of kind Lft -> Type, and then have

fn next<'a>(&'a mut self) -> Option<Self::Item<'a>>

This flexibility is needed, e.g., when writing a safe Doubly-Linked-List with Rc and RefCell, and I also recently wrote a blog post that defined a "stream" API that was essentially this, except that it had to work with current Rust as thus ended up being rather convoluted... Here's the crate doc http://burntsushi.net/rustdoc/fst/trait.Streamer.html. The blog post that linked there is http://blog.burntsushi.net/transducers/.

@ticki

This comment has been minimized.

Copy link
Contributor

ticki commented Dec 8, 2015

One thing that's important is consistencies. HKT is a very powerful concepts, and will probably be used a lot when (if?) they're introduced. Having syntax and semantics which is consistent with Rust's type system is important for such a big change.

Another subject to discuss is how such a change will affect the libraries. How will libstd look with HKTs?

@ticki

This comment has been minimized.

Copy link
Contributor

ticki commented Dec 8, 2015

@antonkatz

This comment has been minimized.

Copy link

antonkatz commented Dec 8, 2015

My humble opinion is that HKT is what brings Rust into a whole different league. Rust might just become a contender in the functional programming community.

@ticki

This comment has been minimized.

Copy link
Contributor

ticki commented Dec 9, 2015

@antonkatz I'm not sure you know how powerful HKTs are. Rust already got generics, but this is just a major addition to generics.

@burdges

This comment has been minimized.

Copy link

burdges commented Dec 9, 2015

In general, higher-kinded polymorphism allows functions to be generic over a type constructors. And rust has many type constructors. :)

As @RalfJung said, they allows functions to be generic over the specific pointer management technique, which suggests they might simplify the contentious situation around allocators.

In Haskell, HTKs appear pretty central to handling errors with type constructors, so they might help simplify exception looking constructs in Rust too.

Also, it appears HKTs would improve dealing with Rust's different types of closures too, which I suspect is what @antonkatz means.

@antonkatz

This comment has been minimized.

Copy link

antonkatz commented Dec 10, 2015

@ticki you're right. I probably don't.
I've looked at learning Rust, but this was a deal breaker. I don't often use HKTs, but once in a while, I encounter a problem that can be solved very nicely with HKTs.
What I meant by my comment, is that it's hard to ask a functional programmer to write code without HKTs.

@BardurArantsson

This comment has been minimized.

Copy link

BardurArantsson commented Sep 25, 2016

I can only concur with @withoutboats ... I'm a Haskeller and my (current) thinking is that MPTC are entirely uncontroversial. (Not that MPTCs have much to do with HKTs, at least as far as I understand it)

@withoutboats

This comment has been minimized.

Copy link
Contributor

withoutboats commented Sep 25, 2016

I vaguely agree with this sentiment, though:

focus on improving associated types or in Haskell terms type families.

I think adding more flexibility in the associated items of a trait is more important than adding more flexibility in parameter kinds. For example, I often want higher kinded associated items, but rarely want other forms of higher kinded polymorphism. I want associated consts to be stabilized more often than I want const parameters. I'm not saying more parameter kinds can't be beneficial, but I've found associated items to really be 'where its at' in my experience.

@Centril

This comment has been minimized.

Copy link
Contributor

Centril commented Sep 25, 2016

As established by @pthariensflame, Rust already has MPTCs, with one example being the From trait.

What Rust lacks atm is the ability to define type constructors of higher kindedness - i.e: (k_1 -> k_2) -> k_3 as opposed to * -> ... -> * - and this doesn't just apply to traits - maybe it would be useful with higher kindedness with structs as well.

For consistencies sake - if higher kindedness is added to the language, then it should be supported for MPTCs as well, i.e: more than for Self<T_1, .., T_n>, so also: Output<T_1, .., T_n>. Associated types is another way of providing MPTCs as you can see in my MonadState example above...

@JohnColanduoni

This comment has been minimized.

Copy link

JohnColanduoni commented Oct 5, 2016

I believe it is a good idea - as others have put it - to find examples where HKTs are useful in Rust in a mostly zero-cost way. But I can't say I have anything special to contribute here.

One important example I've come up against is parameterizing structures over different smart pointer types. Particularly at the library level, it is quite useful to have structures which use some kind of reference counting (Rc or Arc) but don't particularly care which. Allowing the user to choose gives us the best of both worlds: cheap reference counting for single-threaded scenarios, and atomic reference counting when Send + Sync are required.

As for avoiding Haskell-style MPTCs, Rust's concept of "Kind" could be based on a sort of "trait signature" as opposed to a function from types to types. For example, you could have the kind of traits with N parameters and M type members.

@ubsan

This comment has been minimized.

Copy link
Contributor

ubsan commented Oct 8, 2016

@JohnColanduoni I know I've used this kind of parametrizing in C++ in order to implement trait object alikes, and I'd like to be able to do the same thing in Rust.

@brendanzab

This comment has been minimized.

Copy link
Member

brendanzab commented Oct 12, 2016

Crazy idea, taking the lead from the Fn traits.

The following are builtins:

trait Type {}
trait <impl Type> -> impl Type {}
trait <impl Type, impl Type> -> impl Type {}
...

These are automatically implemented by the compiler like so:

impl Type for () {}
impl Type for i32 {}
...
impl <impl Type> -> impl Type for Option {}
impl <impl Type, impl Type> -> impl Type for Result {}
...

We also have type level closures:

type MyResult = |T: impl Type| Result<T, MyError>;
type MyI32Result = MyResult<i32>;

We now can define:

trait Functor: <impl Type> -> impl Type {
    fn map<T, U>(self : Self<T>, f: impl Fn(T) -> U) -> Self<U>;
}

impl Functor for Option {
    fn map<T, U>(self : Option<T>, f: impl Fn(T) -> U) -> Option<U> { ... }
}

impl<E> Functor for (|T| Result<T, E>) {
    fn map<T, U>(self : Result<T, E>, f: impl FnMut(T) -> U) -> Result<U, E> { ... }
}

This sacrifices 'prettiness' in order to stay try to keep it consistent with Rust's existing concrete syntax, semantics, and idioms. For example, the impl Types are horribly verbose - but I needed them to stay true to how the trait syntax works... :/

@skeet70

This comment has been minimized.

Copy link

skeet70 commented Oct 19, 2016

Came across kinder, thought it might be of interest to you if you hadn't seen it already. From the README:

Algebraic structure and emulation of higher-order types for Rust

@larroy

This comment has been minimized.

Copy link

larroy commented Oct 27, 2016

I was thinking about this for some time and reached the conclusion that generics of a higher kind requires some kind of generic that is not monomorphised at compile time, ie. if you need to know the size of an object at compile time you can't really do it. Must be the reason why the languages that support this have some sort of runtime. Please correct me if I'm wrong.

@Boscop

This comment has been minimized.

Copy link

Boscop commented Oct 27, 2016

@larroy Many functional languages have HKT, even C++ has template templates. It doesn't require RTTI, they are monomorphized at compile-time.

@withoutboats

This comment has been minimized.

Copy link
Contributor

withoutboats commented Oct 27, 2016

I was thinking about this for some time and reached the conclusion that generics of a higher kind requires some kind of generic that is not monomorphised at compile time, ie. if you need to know the size of an object at compile time you can't really do it. Must be the reason why the languages that support this have some sort of runtime. Please correct me if I'm wrong.

Types using higher kinded polymorphism can be monomorphized. On the other hand, I think higher rank types can't be monomorphized. On the other other hand, higher rank trait bounds with type parameters are fine. So its specifically a type like for<T> fn(T) -> T.

There are other people who would know better than me though.

@eddyb

This comment has been minimized.

Copy link
Member

eddyb commented Oct 27, 2016

@withoutboats With fn being a function pointer in Rust, perhaps Box<for<T> Fn(T) -> T> fits better?
It's equivalent to having a trait with a generic method, which is already banned from trait objects.
Rust could generate RTTI-style code for that, I suppose - well, you can cheat with function pointers too, so maybe I'm making an unnecessary distinction. I agree either way with the premise.

@jmegaffin

This comment has been minimized.

Copy link

jmegaffin commented Dec 28, 2016

@larroy @withoutboats Implementing higher-rank types (and polymorphic recursion) requires no RTTI. You just have to make sure values of the quantified type are behind pointers, like Rust already does with exotically-sized types or trait objects. So we can say for<T> fn(T) -> T is illegal but for<T> fn(&T) -> &T, for<T> fn(Box<T>) -> Box<T>, etc. are fine.

@withoutboats

This comment has been minimized.

Copy link
Contributor

withoutboats commented Dec 28, 2016

@jmegaffin That's interesting; how would they be represented?

I'm not sure its worth the complexity cost but its cool.

@pthariensflame

This comment has been minimized.

Copy link
Contributor

pthariensflame commented Dec 29, 2016

@jmegaffin Keep in mind that the existence of unsized types in Rust actually screws up that scheme in its naïve form, because of fat pointers.

@jmegaffin

This comment has been minimized.

Copy link

jmegaffin commented Dec 29, 2016

@withoutboats Not considering the fat pointer issue, pointers are fundamentally untyped. Thus, it would be represented exactly as written, plus pointers to vtables for any non-marker traits T is constrained to implement. Again, that doesn't take fat pointers into account.

@pthariensflame Yeah, I suppose it works for e.g. for<T: Sized> fn(&T) -> &T but there needs to be more to it otherwise. There's always the possibility of adding an extra level of indirection (for<T> fn(&&T) -> &&T) but that doesn't strike me as being particularly satisfying.

Perhaps it was a mistake for exotically-sized types to bubble up information to their pointers. It's not strictly necessary, after all.

@glaebhoerl

This comment has been minimized.

Copy link
Contributor

glaebhoerl commented Dec 29, 2016

It's not enough to make sure they're behind pointers in the fn signature, you also have to make sure they're not used in any way that's not behind-a-pointer in the function body either (copied to the stack, calling another function that's not "only-behind-pointers-safe", etc.). Unless you handle size/alignment completely dynamically...

@jmegaffin

This comment has been minimized.

Copy link

jmegaffin commented Dec 29, 2016

@glaebhoerl I don't see any issue with doing it dynamically. Performance will be a little worse, of course, but that's unavoidable. It's only not worse in languages like Haskell because they don't do any monomorphization in the first place.

If you want to use a generic function polymorphically, you just compile it to be dynamic where necessary. Any generic functions that are called that depend on a polymorphic type must in turn be compiled to be dynamic and then used at that call site. That should be enough.

@withoutboats

This comment has been minimized.

Copy link
Contributor

withoutboats commented Jan 3, 2017

It sounds like we'd have to restrict this to object safe bounds and represent this the same as trait objects - that is for<T: Read> fn(&T) would be no different in representation from fn(&Read). In which case I don't see any expressive difference from trait objects either.

@glaebhoerl

This comment has been minimized.

Copy link
Contributor

glaebhoerl commented Jan 4, 2017

@jmegaffin Wait. If you handle size/alignment dynamically then do you even need the "behind a pointer" restriction in the signature? I would think that "statically require every type-instantiation to have the same representation" ("behind a pointer") so that you don't need anything extra at runtime (i.e. analogous to the uniform boxed representation of most extant GCed languages), and "actually deal with all the statically-unknown representations at runtime instead" (so-called "intensional type analysis"), are two disjoint approaches.

@DemiMarie

This comment has been minimized.

Copy link

DemiMarie commented Apr 10, 2017

What about higher kinded lifetimes?

Lifetime parameters are always erased, so the problems with runtime representation simply vanish. And the problem can be much harder to work around, often requiring unnecessary copying.

@tioover

This comment has been minimized.

Copy link

tioover commented Sep 18, 2017

RFC #1598 is merged

This is an incremental step toward a more general feature commonly called "higher-kinded types," which is often ranked highly as a requested feature by Rust users.

@Centril Centril added the T-lang label Feb 23, 2018

@cruhl

This comment has been minimized.

Copy link

cruhl commented Oct 1, 2018

Has there been any recent movement around this issue?

@steveklabnik

This comment has been minimized.

Copy link
Member

steveklabnik commented Oct 1, 2018

The foundational work in the compiler is being done to enable this. So not directly, but in a broad sense, yes.

@skeet70

This comment has been minimized.

Copy link

skeet70 commented Oct 15, 2018

@steveklabnik any links to what work is ahead before HKTs would be directly possible? I tried searching around in the rust issues and rfcs but couldn't figure out what the status is.

@withoutboats

This comment has been minimized.

Copy link
Contributor

withoutboats commented Oct 15, 2018

@steveklabnik any links to what work is ahead before HKTs would be directly possible? I tried searching around in the rust issues and rfcs but couldn't figure out what the status is.

The answer depends on what you mean by "directly." Work is underway to implement generic associated types (GATs), described in RFC #1598 and tracked in this tracking issue: rust-lang/rust#44265 Generic associated types are capable of representing the same type relationships as the higher kinded type class polymorphism in Haskell (for example) - what is normally called "HKT". But its not exactly the same feature, its just equally expressive.

There is no work underway on introducing a mechanism for polymorphism over higher kinded terms other than through generic associated types, and I don't currently see much impetus to add a separate faculty other than GATs.

Niko Matsakis wrote a blog series exploring the relationship between GATs and HKT, the last post contains links to all the previous ones: http://smallcultfollowing.com/babysteps/blog/2016/11/09/associated-type-constructors-part-4-unifying-atc-and-hkt/ (he calls GATs "ATCs" in that series)

EDIT: Looking at Niko's blog post again, I strongly recommend it. Its an excellent representation of our thinking about HKT in Rust right now.

@steveklabnik

This comment has been minimized.

Copy link
Member

steveklabnik commented Oct 15, 2018

@skeet70 i was typing a response but @withoutboats just beat me to it; GATs are not exactly this but can provide similar things.

"impl Trait in Traits" is also, in my understanding, equivalent to HKT.

It is true that we have no immediate plans to offer HKT directly at the time.

wycats pushed a commit to wycats/rust-rfcs that referenced this issue Mar 5, 2019

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.