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 · 91 comments

Comments

Projects
None yet
@rust-highfive

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.

Show comment
Hide comment
@larroy

larroy 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

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.

Show comment
Hide comment
@AlexanderKaraberov

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

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.

Show comment
Hide comment
@Gankro

Gankro Jan 2, 2015

Contributor

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.

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.

Show comment
Hide comment
@huonw

huonw Jul 1, 2015

Member

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

Member

huonw commented Jul 1, 2015

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

@int-index

This comment has been minimized.

Show comment
Hide comment
@int-index

int-index Aug 21, 2015

Why is no one working on this?

int-index commented Aug 21, 2015

Why is no one working on this?

@antonkatz

This comment has been minimized.

Show comment
Hide comment
@antonkatz

antonkatz Sep 11, 2015

Are there any status updates at this time?

antonkatz commented Sep 11, 2015

Are there any status updates at this time?

@steveklabnik

This comment has been minimized.

Show comment
Hide comment
@steveklabnik

steveklabnik Sep 11, 2015

Member

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

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.

Show comment
Hide comment
@ticki

ticki Dec 7, 2015

Contributor

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

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.

Show comment
Hide comment
@steveklabnik

steveklabnik Dec 7, 2015

Member

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

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.

Show comment
Hide comment
@tiziano88

tiziano88 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> { ... }
}

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.

Show comment
Hide comment
@glaebhoerl

glaebhoerl Dec 8, 2015

Contributor

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.

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.

Show comment
Hide comment
@tiziano88

tiziano88 commented Dec 8, 2015

@comex

This comment has been minimized.

Show comment
Hide comment
@comex

comex 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

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.

Show comment
Hide comment
@int-index

int-index Dec 8, 2015

Wadler's law in action...

int-index commented Dec 8, 2015

Wadler's law in action...

@tiziano88

This comment has been minimized.

Show comment
Hide comment
@tiziano88

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

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.

Show comment
Hide comment
@brendanzab

brendanzab Dec 8, 2015

Member

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?

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.

Show comment
Hide comment
@tiziano88

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

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.

Show comment
Hide comment
@brendanzab

brendanzab Dec 8, 2015

Member

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.

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.

Show comment
Hide comment
@RalfJung

RalfJung Dec 8, 2015

Member

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

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.

Show comment
Hide comment
@ticki

ticki Dec 8, 2015

Contributor

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?

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.

Show comment
Hide comment
@ticki
Contributor

ticki commented Dec 8, 2015

@antonkatz

This comment has been minimized.

Show comment
Hide comment
@antonkatz

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

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.

Show comment
Hide comment
@ticki

ticki Dec 9, 2015

Contributor

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

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.

Show comment
Hide comment
@burdges

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

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.

Show comment
Hide comment
@antonkatz

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

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.

@ticki

This comment has been minimized.

Show comment
Hide comment
@ticki

ticki Dec 10, 2015

Contributor

@antonkatz Right, HKTs might render big parts of the API obsolete, so sure adding HKTs is a big thing. But they provide a really nifty abstraction allowing very clean API. But I do believe the change is worth it.

So sure, adding HKTs is a drastic move, but due to the power of them, it's worth the price.

Contributor

ticki commented Dec 10, 2015

@antonkatz Right, HKTs might render big parts of the API obsolete, so sure adding HKTs is a big thing. But they provide a really nifty abstraction allowing very clean API. But I do believe the change is worth it.

So sure, adding HKTs is a drastic move, but due to the power of them, it's worth the price.

@ticki

This comment has been minimized.

Show comment
Hide comment
@ticki

ticki Dec 10, 2015

Contributor

What I meant by my comment, is that it's hard to ask a functional programmer to write code without HKTs.

I see. I must have misunderstood the comment.

Contributor

ticki commented Dec 10, 2015

What I meant by my comment, is that it's hard to ask a functional programmer to write code without HKTs.

I see. I must have misunderstood the comment.

@bluss

This comment has been minimized.

Show comment
Hide comment
@bluss

bluss Dec 10, 2015

I experience the lack of HKT mostly for lifetimes; I would be very happy with just a restriction that enables types to be higher-kinded over just lifetime params.

These are needed for encoding read-only or read-write iterators into a trait in a way that's composable (can recursively pass down a value and interchangeably iterate it and mutate it).

bluss commented Dec 10, 2015

I experience the lack of HKT mostly for lifetimes; I would be very happy with just a restriction that enables types to be higher-kinded over just lifetime params.

These are needed for encoding read-only or read-write iterators into a trait in a way that's composable (can recursively pass down a value and interchangeably iterate it and mutate it).

@steveklabnik

This comment has been minimized.

Show comment
Hide comment
@steveklabnik

steveklabnik Dec 10, 2015

Member

HKTs might render big parts of the API obsolete

We kept HKT in mind when stabilizing libstd, so hopefully this is not true, or at least, on a big scale.

Member

steveklabnik commented Dec 10, 2015

HKTs might render big parts of the API obsolete

We kept HKT in mind when stabilizing libstd, so hopefully this is not true, or at least, on a big scale.

@ticki

This comment has been minimized.

Show comment
Hide comment
@ticki

ticki Dec 10, 2015

Contributor

@nikomatsakis has some interesting thoughts about HKTs here: #213 (comment)

Contributor

ticki commented Dec 10, 2015

@nikomatsakis has some interesting thoughts about HKTs here: #213 (comment)

@ghost

This comment has been minimized.

Show comment
Hide comment
@ghost

ghost Dec 29, 2015

Just chiming in on syntax because I've been running into HKT struggles lately. I think that it might be desirable to show HKT as type functions explicitly. Examples:

// HashMap<K, V> reads as "HashMap has a K, V" whereas |K, V| Map reads as "K, V completes a Map"
trait |T| Monad {
    fn return(obj: T) -> Self<T>;
    fn apply<U>(&mut self: Self<Start>, f: F) -> Self<U> where F: FnOnce(T) -> Self<U>;
}

// |T| Option<T> reads as "with T, we can create an Option with T"
impl Monad for |T| Option<T> {
    fn return(obj: T) -> Option<T> { Some(obj) }
    fn apply<Start, End, F>(&mut self: Self<Start>, f: F) -> Option<End> where F: FnOnce(Start) -> Self<End> { self.and_then(f) }
}

trait Iterable {
    // with a lifetime, we can create an Iter
    type |'a| Iter;
    fn iter<'a>(&self) -> Self::Iter<'a>
}

impl<T> Iterable for Vec<T> {
    // an iter is a function which takes a lifetime and returns a vec::Iter<T>
    type Iter = |'a| vec::Iter<'a, T>;
}

fn apply_twice<M: Monad, A>(start: M<A>, f: F) where F: Fn(A) -> M<A> {
    start.apply(f).apply(f)
}

fn iterate<I, T: Iterable<Iter=I>>(val: &T) where <I as Iterator>::Item: Debug {
    for item in val.iter() {
        println!("iterating: {}", item);
    }
}

fn main() {
    let val = Option::return(5);
    assert_eq!(apply_twice(val, |x| Some(x + 1)), Some(7));
}

Note that the |T| syntax uses the same syntax as <T>, besides the surrounding characters. This means that it also supports bounds. Additionally, type parameters which are higher-kinded are written using the |U| V syntax, if I can find a reason why we need those. (e.g. fn thing<|U| V>)

Also haven't thought about how HKT trait objects would work at all. I considered type Iter<'a> but I feel like making HKT look alike with each other and distinctly different from other types is a plus.

For now, the syntax doesn't support simplified lambdas (i.e. |T| Option<T> as Option) due to the number of weird rules with elision and default parameters, and the fact that we don't know how to refer to the argument type, but there may be solutions to add that in after the fact. I feel like any system would fundamentally have that problem, though.

ghost commented Dec 29, 2015

Just chiming in on syntax because I've been running into HKT struggles lately. I think that it might be desirable to show HKT as type functions explicitly. Examples:

// HashMap<K, V> reads as "HashMap has a K, V" whereas |K, V| Map reads as "K, V completes a Map"
trait |T| Monad {
    fn return(obj: T) -> Self<T>;
    fn apply<U>(&mut self: Self<Start>, f: F) -> Self<U> where F: FnOnce(T) -> Self<U>;
}

// |T| Option<T> reads as "with T, we can create an Option with T"
impl Monad for |T| Option<T> {
    fn return(obj: T) -> Option<T> { Some(obj) }
    fn apply<Start, End, F>(&mut self: Self<Start>, f: F) -> Option<End> where F: FnOnce(Start) -> Self<End> { self.and_then(f) }
}

trait Iterable {
    // with a lifetime, we can create an Iter
    type |'a| Iter;
    fn iter<'a>(&self) -> Self::Iter<'a>
}

impl<T> Iterable for Vec<T> {
    // an iter is a function which takes a lifetime and returns a vec::Iter<T>
    type Iter = |'a| vec::Iter<'a, T>;
}

fn apply_twice<M: Monad, A>(start: M<A>, f: F) where F: Fn(A) -> M<A> {
    start.apply(f).apply(f)
}

fn iterate<I, T: Iterable<Iter=I>>(val: &T) where <I as Iterator>::Item: Debug {
    for item in val.iter() {
        println!("iterating: {}", item);
    }
}

fn main() {
    let val = Option::return(5);
    assert_eq!(apply_twice(val, |x| Some(x + 1)), Some(7));
}

Note that the |T| syntax uses the same syntax as <T>, besides the surrounding characters. This means that it also supports bounds. Additionally, type parameters which are higher-kinded are written using the |U| V syntax, if I can find a reason why we need those. (e.g. fn thing<|U| V>)

Also haven't thought about how HKT trait objects would work at all. I considered type Iter<'a> but I feel like making HKT look alike with each other and distinctly different from other types is a plus.

For now, the syntax doesn't support simplified lambdas (i.e. |T| Option<T> as Option) due to the number of weird rules with elision and default parameters, and the fact that we don't know how to refer to the argument type, but there may be solutions to add that in after the fact. I feel like any system would fundamentally have that problem, though.

@burdges

This comment has been minimized.

Show comment
Hide comment
@burdges

burdges Jan 20, 2016

I'd agree that |T| meshes better with @nikomatsakis's concerns about currying and lifetimes #213 (comment)

I hope Rust avoids the functor morass in OCaML and goes directly for HKTs. It worth checking out section 1.1 here https://ocamllabs.github.io/higher/lightweight-higher-kinded-polymorphism.pdf and maybe https://github.com/ocamllabs/higher

burdges commented Jan 20, 2016

I'd agree that |T| meshes better with @nikomatsakis's concerns about currying and lifetimes #213 (comment)

I hope Rust avoids the functor morass in OCaML and goes directly for HKTs. It worth checking out section 1.1 here https://ocamllabs.github.io/higher/lightweight-higher-kinded-polymorphism.pdf and maybe https://github.com/ocamllabs/higher

@ticki

This comment has been minimized.

Show comment
Hide comment
@ticki

ticki Jan 20, 2016

Contributor

I don't like the |T| syntax, because it is easy to confuse with closures.

Contributor

ticki commented Jan 20, 2016

I don't like the |T| syntax, because it is easy to confuse with closures.

@tiziano88

This comment has been minimized.

Show comment
Hide comment
@tiziano88

tiziano88 Jan 20, 2016

I also like the idea of different syntax for type variable introduction |T| and consumption <T>, and it looks like it will indeed make it easy to express HKT in some cases.

@ticki the point is exactly that it mimics the syntax used to introduce arguments in closures, but in the context of type signature only individual type variables may be contained, so there should be no risk of confusion.

tiziano88 commented Jan 20, 2016

I also like the idea of different syntax for type variable introduction |T| and consumption <T>, and it looks like it will indeed make it easy to express HKT in some cases.

@ticki the point is exactly that it mimics the syntax used to introduce arguments in closures, but in the context of type signature only individual type variables may be contained, so there should be no risk of confusion.

@burdges

This comment has been minimized.

Show comment
Hide comment
@burdges

burdges Jan 20, 2016

Appears HKT in https://gist.github.com/14427/af90a21b917d2892eace meets @nikomatsakis's restriction that the higher-kinded type be a partially applied type, right? It's maybe worth patching it or similar with more explanatory type variable names and writing up a version using the trait |T| ... syntax, or the trait<T> ... syntax if that's your shade.

As an aside, I have not yet really understood the problems that come from using the mere presences of unbound type variables to denote higher-kinds like in Haskell https://hackage.haskell.org/package/base-4.8.1.0/docs/src/GHC.Base.html#Functor I donno if that "[tethers] the compiler's type inference .. to trait resolution". Ignoring that, couldn't one impose the partially applied type restriction at the impl site somehow without necessarily asking for explicit type variables? It's maybe an issue that Rust already assigns a meaning to lifetimes being unbound elsewhere.

burdges commented Jan 20, 2016

Appears HKT in https://gist.github.com/14427/af90a21b917d2892eace meets @nikomatsakis's restriction that the higher-kinded type be a partially applied type, right? It's maybe worth patching it or similar with more explanatory type variable names and writing up a version using the trait |T| ... syntax, or the trait<T> ... syntax if that's your shade.

As an aside, I have not yet really understood the problems that come from using the mere presences of unbound type variables to denote higher-kinds like in Haskell https://hackage.haskell.org/package/base-4.8.1.0/docs/src/GHC.Base.html#Functor I donno if that "[tethers] the compiler's type inference .. to trait resolution". Ignoring that, couldn't one impose the partially applied type restriction at the impl site somehow without necessarily asking for explicit type variables? It's maybe an issue that Rust already assigns a meaning to lifetimes being unbound elsewhere.

@raulraja

This comment has been minimized.

Show comment
Hide comment
@raulraja

raulraja Sep 20, 2016

@burdges Scala examples but nonetheless explanatory of the importance of HKTs http://typelevel.org/blog/2016/08/21/hkts-moving-forward.html

raulraja commented Sep 20, 2016

@burdges Scala examples but nonetheless explanatory of the importance of HKTs http://typelevel.org/blog/2016/08/21/hkts-moving-forward.html

@infinity0

This comment has been minimized.

Show comment
Hide comment
@infinity0

infinity0 Sep 20, 2016

@raulraja you missed the point that @burdges and others have been making. Yes, everyone here including Rust language developers already knows the benefits of HKTs and what they can be used for. The problem is to integrate this well with Rust's other features.

If you want to help improve the situation, you need to study how Rust's other features interact with HKT. The link you posted is below and before this level; it does not help to solve these problems, nor does it educate us on how to solve these problems. Repeatedly posting about what everyone already knows, is not useful. Other people joining this thread, please also do some reading before you post.

infinity0 commented Sep 20, 2016

@raulraja you missed the point that @burdges and others have been making. Yes, everyone here including Rust language developers already knows the benefits of HKTs and what they can be used for. The problem is to integrate this well with Rust's other features.

If you want to help improve the situation, you need to study how Rust's other features interact with HKT. The link you posted is below and before this level; it does not help to solve these problems, nor does it educate us on how to solve these problems. Repeatedly posting about what everyone already knows, is not useful. Other people joining this thread, please also do some reading before you post.

@raulraja

This comment has been minimized.

Show comment
Hide comment
@raulraja

raulraja Sep 20, 2016

@infinity0 the link I posted is what many folks would expect from HKTs, I'm pretty sure is useful for some folks thinking about introducing this feature to the Lang. When you search for rust HKTs you land here. That is how I found this conversation. I don't know about the internals but you seem to be implying I should learn them to express my desire to have HKTs in rust. I obviously don't. Feel free to remove my post if you have the authority or think it's out of place in this forum. Also I recommend you read up on manners when addressing newcomers to this community as my intention was only to stress the importance of this feature to the FP programming community at large and not how to best implement it in rust. Many folks come here and don't see the obvious advantage of such a feature. That link will shed some light. @burdges my apologies for misunderstanding you.

raulraja commented Sep 20, 2016

@infinity0 the link I posted is what many folks would expect from HKTs, I'm pretty sure is useful for some folks thinking about introducing this feature to the Lang. When you search for rust HKTs you land here. That is how I found this conversation. I don't know about the internals but you seem to be implying I should learn them to express my desire to have HKTs in rust. I obviously don't. Feel free to remove my post if you have the authority or think it's out of place in this forum. Also I recommend you read up on manners when addressing newcomers to this community as my intention was only to stress the importance of this feature to the FP programming community at large and not how to best implement it in rust. Many folks come here and don't see the obvious advantage of such a feature. That link will shed some light. @burdges my apologies for misunderstanding you.

@burdges

This comment has been minimized.

Show comment
Hide comment
@burdges

burdges Sep 20, 2016

At least in the ? as do notation discussion, it felt to me like all the monads I knew about fell into a couple categories that needed different flow control implementations. The first was the carrier trait version of ? for Result plus some sort of state. You could for example build a transactional monad in that set up. The second was some form of collections, so like if you continued using ? then it would denote a for loop over an iterator or something. In say Haskell, some of these things would be handled by building up closures, which seems non-zero cost.

Anyways, there are two recently accepted RFCs for more limited subsets of HKT :

First, impl Trait improves polymorphism over unboxed return types. And futures-rs already recommends using them to write code that's polymorphic over future types.

Interestingly doing impl Trait for trait fns gives HKTs, so maybe a useful exercise would be working out what HKT patterns would be non-zero cost if implemented using some hypothetical impl Trait syntax for trait fns in some naive way. I'd suspect collection monads fit the bill, which might've implications for iterators, although maybe something simpler.

Second, associated type constructors are HTKs with only a lifetime parameter, which improves borrowing when using trait fns.

burdges commented Sep 20, 2016

At least in the ? as do notation discussion, it felt to me like all the monads I knew about fell into a couple categories that needed different flow control implementations. The first was the carrier trait version of ? for Result plus some sort of state. You could for example build a transactional monad in that set up. The second was some form of collections, so like if you continued using ? then it would denote a for loop over an iterator or something. In say Haskell, some of these things would be handled by building up closures, which seems non-zero cost.

Anyways, there are two recently accepted RFCs for more limited subsets of HKT :

First, impl Trait improves polymorphism over unboxed return types. And futures-rs already recommends using them to write code that's polymorphic over future types.

Interestingly doing impl Trait for trait fns gives HKTs, so maybe a useful exercise would be working out what HKT patterns would be non-zero cost if implemented using some hypothetical impl Trait syntax for trait fns in some naive way. I'd suspect collection monads fit the bill, which might've implications for iterators, although maybe something simpler.

Second, associated type constructors are HTKs with only a lifetime parameter, which improves borrowing when using trait fns.

@tbenst

This comment has been minimized.

Show comment
Hide comment
@tbenst

tbenst Sep 20, 2016

@raulraja thx I think we are of the same mind. @infinity0 @burdges glad to hear that the rust community recognizes the value of HKT and that we can move past this part of the discussion.

It looks like rust already has many of the pieces needed for HKT: generics, closures, function typing, etc. I'm not familiar with the rust internals but we can discuss how the following function might be implemented and go through the type checker?

def compose(*functions):
   """return a functional composition of given functions.

    compose(f,g,h)(x) is equivalent to h(g(f(x)))"""

    return functools.reduce(lambda f, g: lambda x: g(f(x)), functions)

For context:

I do neuroscience research and use the following function as the key foundation to analyze 1000s of neurons concurrently. I'm interested in rust as it is low-level and more suitable than python for real-time processing with consistent memory usage and runtime performance.

def apply_pipeline(pipeline, units):
    """Apply the function pipeline to the spike train (List[float]) of each neuron."""
    return {k: pipeline(v.spike_train) for k,v in units.items()}

This is particularly powerful when combined with function factories (connoted by 'f_'):

def f_filter(function):
    def filter_dict(f,d):
        for key, val in d.items():
            if not f(key,val):
                continue
            yield key, val

    def anonymous(x):
        if type(x) is list:
            return list(filter(function, x))
        elif type(x) is dict:
            return {k:v for (k,v) in filter_dict(function,x)}

    return anonymous

def f_has_stimulus_type(stimulus_type: Union[str,List[str]]):
    if type(stimulus_type) is str:
        stimulus_type = [stimulus_type]
    def anonymous(e):
        if (e["stimulus"]["stimulusType"] in stimulus_type ):
            return True
        else:
            return False
    return anonymous

The typical workflow uses compose to construct a pipeline, which is then used repeatedly. I've included actual functions to illustrate how meta-parameters (Edit: changed to also show passing a function as an argument to f_filter) can be passed to modify a function, but their actual definitions are irrelevant for the purposes of this example.

get_ifr_by_stimulus = compose(
    f_create_experiments(stimulus_list),
    f_instantaneous_firing_rate(sigma=1,bin_width=0.001,bandwidth=0.1),
    f_filter(f_has_stimulus_type(["GRATING", "BAR'])),
    group_by_stimulus(),
)

firing_rate_1 = apply_pipeline(get_ifr_by_stimulus, data1)
firing_rate_2 = apply_pipeline(get_ifr_by_stimulus, data2)
...etc...

tbenst commented Sep 20, 2016

@raulraja thx I think we are of the same mind. @infinity0 @burdges glad to hear that the rust community recognizes the value of HKT and that we can move past this part of the discussion.

It looks like rust already has many of the pieces needed for HKT: generics, closures, function typing, etc. I'm not familiar with the rust internals but we can discuss how the following function might be implemented and go through the type checker?

def compose(*functions):
   """return a functional composition of given functions.

    compose(f,g,h)(x) is equivalent to h(g(f(x)))"""

    return functools.reduce(lambda f, g: lambda x: g(f(x)), functions)

For context:

I do neuroscience research and use the following function as the key foundation to analyze 1000s of neurons concurrently. I'm interested in rust as it is low-level and more suitable than python for real-time processing with consistent memory usage and runtime performance.

def apply_pipeline(pipeline, units):
    """Apply the function pipeline to the spike train (List[float]) of each neuron."""
    return {k: pipeline(v.spike_train) for k,v in units.items()}

This is particularly powerful when combined with function factories (connoted by 'f_'):

def f_filter(function):
    def filter_dict(f,d):
        for key, val in d.items():
            if not f(key,val):
                continue
            yield key, val

    def anonymous(x):
        if type(x) is list:
            return list(filter(function, x))
        elif type(x) is dict:
            return {k:v for (k,v) in filter_dict(function,x)}

    return anonymous

def f_has_stimulus_type(stimulus_type: Union[str,List[str]]):
    if type(stimulus_type) is str:
        stimulus_type = [stimulus_type]
    def anonymous(e):
        if (e["stimulus"]["stimulusType"] in stimulus_type ):
            return True
        else:
            return False
    return anonymous

The typical workflow uses compose to construct a pipeline, which is then used repeatedly. I've included actual functions to illustrate how meta-parameters (Edit: changed to also show passing a function as an argument to f_filter) can be passed to modify a function, but their actual definitions are irrelevant for the purposes of this example.

get_ifr_by_stimulus = compose(
    f_create_experiments(stimulus_list),
    f_instantaneous_firing_rate(sigma=1,bin_width=0.001,bandwidth=0.1),
    f_filter(f_has_stimulus_type(["GRATING", "BAR'])),
    group_by_stimulus(),
)

firing_rate_1 = apply_pipeline(get_ifr_by_stimulus, data1)
firing_rate_2 = apply_pipeline(get_ifr_by_stimulus, data2)
...etc...
@eddyb

This comment has been minimized.

Show comment
Hide comment
@eddyb

eddyb Sep 20, 2016

Member

Interestingly doing impl Trait for trait fns gives HKTs

That's a red herring: it's not the same feature as the accepted RFC, but rather sugar for associated types:

trait Foo {
    fn foo<T>() -> impl Trait;
}
// is sugar for:
trait Foo {
    type Assoc<T>;
    fn foo<T>() -> Self::Assoc<T>;
}

This is a non-essential feature, and getting it before higher-kinded associated types has all the risks of adding HKT to Rust, with an extra layer of obscurity which will cause soundness issues to lurk for longer.

Member

eddyb commented Sep 20, 2016

Interestingly doing impl Trait for trait fns gives HKTs

That's a red herring: it's not the same feature as the accepted RFC, but rather sugar for associated types:

trait Foo {
    fn foo<T>() -> impl Trait;
}
// is sugar for:
trait Foo {
    type Assoc<T>;
    fn foo<T>() -> Self::Assoc<T>;
}

This is a non-essential feature, and getting it before higher-kinded associated types has all the risks of adding HKT to Rust, with an extra layer of obscurity which will cause soundness issues to lurk for longer.

@eddyb

This comment has been minimized.

Show comment
Hide comment
@eddyb

eddyb Sep 20, 2016

Member

@tbenst That has absolutely nothing to do with HKT:

fn compose<T, U, V, F: FnMut(T) -> U, G: FnMut(U) -> V>(mut f: F, mut g: G)
                                                        -> impl FnMut(T) -> V {
    move |x| g(f(x))
}

Making it variadic requires VG (variadic generics), but that's also orthogonal to HKT.

What you're doing seems to involve iterators anyway, so you might want combinators for those instead.

Member

eddyb commented Sep 20, 2016

@tbenst That has absolutely nothing to do with HKT:

fn compose<T, U, V, F: FnMut(T) -> U, G: FnMut(U) -> V>(mut f: F, mut g: G)
                                                        -> impl FnMut(T) -> V {
    move |x| g(f(x))
}

Making it variadic requires VG (variadic generics), but that's also orthogonal to HKT.

What you're doing seems to involve iterators anyway, so you might want combinators for those instead.

@withoutboats

This comment has been minimized.

Show comment
Hide comment
@withoutboats

withoutboats Sep 20, 2016

Contributor

@tbenst like eddyb, I don't see anything in your post that involves higher kinded polymorphism. I think it would be to your advantage to try Rust, it may have everything you need already. :-) And if it is missing something, feedback about where you are actually running into the limits of the type system is much easier to take action on.

Contributor

withoutboats commented Sep 20, 2016

@tbenst like eddyb, I don't see anything in your post that involves higher kinded polymorphism. I think it would be to your advantage to try Rust, it may have everything you need already. :-) And if it is missing something, feedback about where you are actually running into the limits of the type system is much easier to take action on.

@tbenst

This comment has been minimized.

Show comment
Hide comment
@tbenst

tbenst Sep 20, 2016

@eddyb awesome, thanks for the example! Admittedly I don't yet understand what you wrote but looks like I have no longer have excuse to delay learning rust ;).

@withoutboats f_filter is a trivial case of polymorphism that operates on both lists and dictionaries, returning a value of the same type. Thus, a downstream function could fail depending on input if it only operates on, say, a list. I make these types of mistakes from time to time in python since there is no type checker. Although it sounds like the rust type checker would already catch this.

tbenst commented Sep 20, 2016

@eddyb awesome, thanks for the example! Admittedly I don't yet understand what you wrote but looks like I have no longer have excuse to delay learning rust ;).

@withoutboats f_filter is a trivial case of polymorphism that operates on both lists and dictionaries, returning a value of the same type. Thus, a downstream function could fail depending on input if it only operates on, say, a list. I make these types of mistakes from time to time in python since there is no type checker. Although it sounds like the rust type checker would already catch this.

@withoutboats

This comment has been minimized.

Show comment
Hide comment
@withoutboats

withoutboats Sep 20, 2016

Contributor

@tbenst Rust's filter is already implemented on Iterator, which is a generic trait that allows you to iterate over many kinds of things, including both vectors and hash maps. Not only are the kinds of errors you're describing caught by the type checker, but Rust's standard library also provides some very compose-able adapters so you wouldn't need to write something like filter_dict.

Contributor

withoutboats commented Sep 20, 2016

@tbenst Rust's filter is already implemented on Iterator, which is a generic trait that allows you to iterate over many kinds of things, including both vectors and hash maps. Not only are the kinds of errors you're describing caught by the type checker, but Rust's standard library also provides some very compose-able adapters so you wouldn't need to write something like filter_dict.

@Centril

This comment has been minimized.

Show comment
Hide comment
@Centril

Centril Sep 22, 2016

Contributor

Since some seem to be wondering what HKTs are ...

Let's talk about Kinds, baby

For the uninitiated, I'll try to do a brief overview of what kinds, and HKTs (Higher Kinded Types) are, and a few choices to consider for HKTs in Rust. Hopefully this can shed some light on the topic for some. Understanding this might require some knowledge of haskell, but I'll try to give Rust equivalents where possible.

What is a Kind?

Kinds can most succinctly be described as "types of types". In this sense, they are higher-order-types.

NOTE: To get the kind of something in ghci, you can use :k <type>, where <type> is the one you want kind information about.

Let's take a look at the most basic kind: *, pronounced: star - which is the kind of all data types, and can also be called "proper types", or the concrete kind. Just as we have constructors for data types, such as:

data Nat = Zero | Succ Nat

-- where

λ> :t Zero
Zero :: Nat
λ> :t Succ
Succ :: Nat -> Nat
λ> :k Nat
Nat :: *
// in Rust - this is a truly inefficient way to implement Nat, but simple to grasp.
enum Nat { Zero, Succ(Box<Nat>) }

, we also have type constructors that take types as arguments and produce another type as a result.
The star kind can thus be seen as a type constructor taking zero arguments, i.e: a nullary type constructor.

Next, if we add generics to the mix, we get other results:

data Maybe a = Nothing | Just a

λ> :k Maybe
Maybe :: * -> *
// See: https://doc.rust-lang.org/std/option/enum.Option.html
pub enum Option<T> {
    None,
    Some(T),
}

As we see, a Maybe, is a unary type constructor from a type (of an element in the Maybe, in this case) to a specific Maybe type, such as Maybe Nat, which has kind star, as seen in: λ> :k Maybe Nat :: *

If we add another generic parameter, we get a binary type constructor. Such an example is the kind of the function type, or the kind of Either (Result in Rust), or a Map (similar to BTreeMap in Rust):

λ> :k (->)
(->) :: * -> * -> *

data Either a b = Left a | Right b
λ> :k Either
Either :: * -> * -> *

λ> :k Map
Map :: * -> * -> *

An aside note: to actually create a Map, the key type must satisfy the Ord trait or type class.

In GHC, there are other kinds which are of no interest, but GHC.Prim.Constraint is of special significance.
This is the kind that you get when you fulfill a type class, which correspond to traits in Rust.
For example, for the Eq type class (Rust equivalent to Eq):

λ> :i Eq
class Eq a where
    (==) :: a -> a -> Bool
    (/=) :: a -> a -> Bool

λ> :k Eq
Eq :: * -> GHC.Prim.Constraint

In this sense, in type theory, both type classes and traits are in fact types.

Higher Kinded Types

So far, everything I've covered is possible in Rust.
But wait, there is more - enter HKTs (Higher Kinded Types).

Higher Order Functions

Just as there are functions taking functions as arguments (or returning functions), which are called HoFs (Higher Order Functions).

Some well known HoFs are: map, fold, filter, (.).

twice :: (a -> a) -> (a -> a)
twice f = f . f
// in Rust:
fn twice<A, F>(function: F) -> Box<Fn(A) -> A> 
    where F: 'static + Fn(A) -> A {
    Box::new(move |a| function(function(a)))
}

Higher Order Type-Constructors

Just as there are HoFs of regular types, there are HoFs of kinds, for example: (k1 -> k2) -> k3

Examples of type classes with that form are:

class Functor f where
    fmap :: (a -> b) -> f a -> f b

-- fully expanded:
class Functor (f :: * -> *) where
    fmap :: (a -> b) -> f a -> f b

λ> :k Functor
Functor :: (* -> *) -> GHC.Prim.Constraint

-- An example instance of Functor for Maybe:
instance Functor Maybe where
    fmap f Nothing  = Nothing
    fmap f (Just a) = Just (f a)
class Functor f => Applicative f where
    pure :: a -> f a
    (<*>) :: f (a -> b) -> f a -> f b

λ> :k Applicative
Applicative :: (* -> *) -> GHC.Prim.Constraint

-- An example instance of Applicative for Maybe:
instance Applicative Maybe where
    pure x = Just x
    Nothing  <*> m = Nothing
    (Just f) <*> m = fmap f m
class Applicative m => Monad m where
    (>>=) :: m a -> (a -> m b) -> m b

λ> :k Monad
Monad :: (* -> *) -> GHC.Prim.Constraint

-- An example instance of Monad for Maybe:
instance  Monad Maybe  where
    (Just x) >>= k = k x
    Nothing  >>= _ = Nothing

These type classes allow us to reason about data structures as such (assuming they are instances of the type class). Here, f and m are as expected generic data structures, or unary type constructors of kind * -> *, allowing us to be generic over f t, where t is a type, for example: f a, f b.

More about monads: http://dev.stephendiehl.com/hask/#monads

FlexibleInstances

FlexibleContexts

TypeSynonymInstances

MultiParamTypeClasses

Consider the case where we want a polymorphic way to convert between data structures, but preserve the elements. How do we encode this?

-- To enable the language feature.
{-# LANGUAGE MultiParamTypeClasses #-}

-- Needed for the Either () instance below:
{-# LANGUAGE FlexibleInstances #-}

-- Needed to be explicit about f, g :: * -> *
{-# LANGUAGE KindSignatures #-}

import Data.Monoid ((<>)) -- for <> operator.

-- Converting between data structures while preserving elements.
class StructureConversion (f :: * -> *) (g :: * -> *) where
    convert :: f a -> g a

-- Maybe and Either () are isomorphic.
instance StructureConversion Maybe (Either ()) where
    convert Nothing  = Left ()
    convert (Just a) = Right a

-- A binary tree:
data BTree a = L | N a (BTree a) (BTree a)
    deriving Show

-- Converting between BTree:s and Lists.
-- NOTE: this is not structure preserving.
-- i.e: converting to list and then back could never guarantee the exact form back.
instance StructureConversion BTree [] where
    convert L         = []
    convert (N e l r) = e : convert l <> convert r

Using the definitions:

λ> convert (Just 42) :: Either () Int
Right 42

λ> let tree = N 1 (N 2 L L) (N 3 (N 4 L L) L) :: BTree Int
λ> convert tree :: [Int]
[1,2,3,4]

FunctionalDependencies

When dealing with type classes with multiple parameters, a functional dependency between the parameters a -> b, means that a uniquely determines b - which means that a1 ≡ a2 -> b1 ≡ b2

An example of such a type class is MonadState:

class Monad m => MonadState s m | m -> s where
    get   :: m s
    put   :: s -> m ()
    state :: (s -> (a, s)) -> m a

Which has kind:

λ> :k MonadState
MonadState :: * -> (* -> *) -> GHC.Prim.Constraint

Not only type classes involve HKTs

An example where data types involve HKTs is StateT (the state transformer monad), which looks like:

newtype StateT s m a = StateT { runStateT :: s -> m (a, s) }

instance Monad m => MonadState s (StateT s m) where
    get     = state  $ \s -> (s, s)
    put   s = state  $ const ((), s)
    state f = StateT $ return . f

instance Monad m => Monad        (StateT s m) where
    return           = state . (,)
    (StateT f) >>= g = StateT $ f >=> \(a, s1) -> runStateT (g a) s1

instance Monad m => Functor      (StateT s m) where
    fmap = liftM

instance Monad m => Applicative  (StateT s m) where
    pure  = return
    (<*>) = ap

And has the kind:

λ> :k StateT
StateT :: * -> (* -> *) -> * -> *

More complex HKTs

Here are some examples of more complex HKTs.

  • MonadTrans, kind: ((* -> *) -> * -> *) -> GHC.Prim.Constraint
class MonadTrans t where
    -- | Lift a computation from the argument monad to the constructed monad.
    lift :: (Monad m) => m a -> t m a

instance MonadTrans (StateT s) where
    lift m = StateT $ \si -> (, si) <$> m
  • Category, kind: (* -> * -> *) -> GHC.Prim.Constraint
class Category cat where
    id :: cat a a
    (.) :: cat b c -> cat a b -> cat a c
class IMonad m where
    ireturn :: a -> m i i a
    (>>>=) :: m i j a -> (a -> m j k b) -> m i k b

A note on syntax

In my opinion, one of the really nice things about type classes in haskell is that they are incredibly terse - as opposed to verbose. While the same terseness is not possible in Rust, it should still be an aim to not let things be overly verbose.

What should be available in Rust?

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. It might also be good to classify these into the various kinds they have, so that we know the needs of the type system... But it might also be a good idea to start from the kinds and then see if anything useful can be made that have those kinds.

  • Should HKTs be possible in structs - which are not traits, as in the StateT example?
  • Should MultiParamTypeClasses (MPTC) and FunctionalDependencies be possible?

MPTCs means that we can't really talk about a Self anymore, since they are rather relations between two or more type constructors. Having MPTCs would therefore probably mean that something different than trait is needed. Here we see that traits and type classes diverge in semantics as traits give you trait objects. The keyword trait could possibly be reused, but in this case, I find it difficult to see how it would be possible to omit the type constructors f, g in class StructureConversion f g - especially not in the case of FunctionalDependencies.

  • Should FlexibleContexts and FlexibleInstances should be possible?
Contributor

Centril commented Sep 22, 2016

Since some seem to be wondering what HKTs are ...

Let's talk about Kinds, baby

For the uninitiated, I'll try to do a brief overview of what kinds, and HKTs (Higher Kinded Types) are, and a few choices to consider for HKTs in Rust. Hopefully this can shed some light on the topic for some. Understanding this might require some knowledge of haskell, but I'll try to give Rust equivalents where possible.

What is a Kind?

Kinds can most succinctly be described as "types of types". In this sense, they are higher-order-types.

NOTE: To get the kind of something in ghci, you can use :k <type>, where <type> is the one you want kind information about.

Let's take a look at the most basic kind: *, pronounced: star - which is the kind of all data types, and can also be called "proper types", or the concrete kind. Just as we have constructors for data types, such as:

data Nat = Zero | Succ Nat

-- where

λ> :t Zero
Zero :: Nat
λ> :t Succ
Succ :: Nat -> Nat
λ> :k Nat
Nat :: *
// in Rust - this is a truly inefficient way to implement Nat, but simple to grasp.
enum Nat { Zero, Succ(Box<Nat>) }

, we also have type constructors that take types as arguments and produce another type as a result.
The star kind can thus be seen as a type constructor taking zero arguments, i.e: a nullary type constructor.

Next, if we add generics to the mix, we get other results:

data Maybe a = Nothing | Just a

λ> :k Maybe
Maybe :: * -> *
// See: https://doc.rust-lang.org/std/option/enum.Option.html
pub enum Option<T> {
    None,
    Some(T),
}

As we see, a Maybe, is a unary type constructor from a type (of an element in the Maybe, in this case) to a specific Maybe type, such as Maybe Nat, which has kind star, as seen in: λ> :k Maybe Nat :: *

If we add another generic parameter, we get a binary type constructor. Such an example is the kind of the function type, or the kind of Either (Result in Rust), or a Map (similar to BTreeMap in Rust):

λ> :k (->)
(->) :: * -> * -> *

data Either a b = Left a | Right b
λ> :k Either
Either :: * -> * -> *

λ> :k Map
Map :: * -> * -> *

An aside note: to actually create a Map, the key type must satisfy the Ord trait or type class.

In GHC, there are other kinds which are of no interest, but GHC.Prim.Constraint is of special significance.
This is the kind that you get when you fulfill a type class, which correspond to traits in Rust.
For example, for the Eq type class (Rust equivalent to Eq):

λ> :i Eq
class Eq a where
    (==) :: a -> a -> Bool
    (/=) :: a -> a -> Bool

λ> :k Eq
Eq :: * -> GHC.Prim.Constraint

In this sense, in type theory, both type classes and traits are in fact types.

Higher Kinded Types

So far, everything I've covered is possible in Rust.
But wait, there is more - enter HKTs (Higher Kinded Types).

Higher Order Functions

Just as there are functions taking functions as arguments (or returning functions), which are called HoFs (Higher Order Functions).

Some well known HoFs are: map, fold, filter, (.).

twice :: (a -> a) -> (a -> a)
twice f = f . f
// in Rust:
fn twice<A, F>(function: F) -> Box<Fn(A) -> A> 
    where F: 'static + Fn(A) -> A {
    Box::new(move |a| function(function(a)))
}

Higher Order Type-Constructors

Just as there are HoFs of regular types, there are HoFs of kinds, for example: (k1 -> k2) -> k3

Examples of type classes with that form are:

class Functor f where
    fmap :: (a -> b) -> f a -> f b

-- fully expanded:
class Functor (f :: * -> *) where
    fmap :: (a -> b) -> f a -> f b

λ> :k Functor
Functor :: (* -> *) -> GHC.Prim.Constraint

-- An example instance of Functor for Maybe:
instance Functor Maybe where
    fmap f Nothing  = Nothing
    fmap f (Just a) = Just (f a)
class Functor f => Applicative f where
    pure :: a -> f a
    (<*>) :: f (a -> b) -> f a -> f b

λ> :k Applicative
Applicative :: (* -> *) -> GHC.Prim.Constraint

-- An example instance of Applicative for Maybe:
instance Applicative Maybe where
    pure x = Just x
    Nothing  <*> m = Nothing
    (Just f) <*> m = fmap f m
class Applicative m => Monad m where
    (>>=) :: m a -> (a -> m b) -> m b

λ> :k Monad
Monad :: (* -> *) -> GHC.Prim.Constraint

-- An example instance of Monad for Maybe:
instance  Monad Maybe  where
    (Just x) >>= k = k x
    Nothing  >>= _ = Nothing

These type classes allow us to reason about data structures as such (assuming they are instances of the type class). Here, f and m are as expected generic data structures, or unary type constructors of kind * -> *, allowing us to be generic over f t, where t is a type, for example: f a, f b.

More about monads: http://dev.stephendiehl.com/hask/#monads

FlexibleInstances

FlexibleContexts

TypeSynonymInstances

MultiParamTypeClasses

Consider the case where we want a polymorphic way to convert between data structures, but preserve the elements. How do we encode this?

-- To enable the language feature.
{-# LANGUAGE MultiParamTypeClasses #-}

-- Needed for the Either () instance below:
{-# LANGUAGE FlexibleInstances #-}

-- Needed to be explicit about f, g :: * -> *
{-# LANGUAGE KindSignatures #-}

import Data.Monoid ((<>)) -- for <> operator.

-- Converting between data structures while preserving elements.
class StructureConversion (f :: * -> *) (g :: * -> *) where
    convert :: f a -> g a

-- Maybe and Either () are isomorphic.
instance StructureConversion Maybe (Either ()) where
    convert Nothing  = Left ()
    convert (Just a) = Right a

-- A binary tree:
data BTree a = L | N a (BTree a) (BTree a)
    deriving Show

-- Converting between BTree:s and Lists.
-- NOTE: this is not structure preserving.
-- i.e: converting to list and then back could never guarantee the exact form back.
instance StructureConversion BTree [] where
    convert L         = []
    convert (N e l r) = e : convert l <> convert r

Using the definitions:

λ> convert (Just 42) :: Either () Int
Right 42

λ> let tree = N 1 (N 2 L L) (N 3 (N 4 L L) L) :: BTree Int
λ> convert tree :: [Int]
[1,2,3,4]

FunctionalDependencies

When dealing with type classes with multiple parameters, a functional dependency between the parameters a -> b, means that a uniquely determines b - which means that a1 ≡ a2 -> b1 ≡ b2

An example of such a type class is MonadState:

class Monad m => MonadState s m | m -> s where
    get   :: m s
    put   :: s -> m ()
    state :: (s -> (a, s)) -> m a

Which has kind:

λ> :k MonadState
MonadState :: * -> (* -> *) -> GHC.Prim.Constraint

Not only type classes involve HKTs

An example where data types involve HKTs is StateT (the state transformer monad), which looks like:

newtype StateT s m a = StateT { runStateT :: s -> m (a, s) }

instance Monad m => MonadState s (StateT s m) where
    get     = state  $ \s -> (s, s)
    put   s = state  $ const ((), s)
    state f = StateT $ return . f

instance Monad m => Monad        (StateT s m) where
    return           = state . (,)
    (StateT f) >>= g = StateT $ f >=> \(a, s1) -> runStateT (g a) s1

instance Monad m => Functor      (StateT s m) where
    fmap = liftM

instance Monad m => Applicative  (StateT s m) where
    pure  = return
    (<*>) = ap

And has the kind:

λ> :k StateT
StateT :: * -> (* -> *) -> * -> *

More complex HKTs

Here are some examples of more complex HKTs.

  • MonadTrans, kind: ((* -> *) -> * -> *) -> GHC.Prim.Constraint
class MonadTrans t where
    -- | Lift a computation from the argument monad to the constructed monad.
    lift :: (Monad m) => m a -> t m a

instance MonadTrans (StateT s) where
    lift m = StateT $ \si -> (, si) <$> m
  • Category, kind: (* -> * -> *) -> GHC.Prim.Constraint
class Category cat where
    id :: cat a a
    (.) :: cat b c -> cat a b -> cat a c
class IMonad m where
    ireturn :: a -> m i i a
    (>>>=) :: m i j a -> (a -> m j k b) -> m i k b

A note on syntax

In my opinion, one of the really nice things about type classes in haskell is that they are incredibly terse - as opposed to verbose. While the same terseness is not possible in Rust, it should still be an aim to not let things be overly verbose.

What should be available in Rust?

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. It might also be good to classify these into the various kinds they have, so that we know the needs of the type system... But it might also be a good idea to start from the kinds and then see if anything useful can be made that have those kinds.

  • Should HKTs be possible in structs - which are not traits, as in the StateT example?
  • Should MultiParamTypeClasses (MPTC) and FunctionalDependencies be possible?

MPTCs means that we can't really talk about a Self anymore, since they are rather relations between two or more type constructors. Having MPTCs would therefore probably mean that something different than trait is needed. Here we see that traits and type classes diverge in semantics as traits give you trait objects. The keyword trait could possibly be reused, but in this case, I find it difficult to see how it would be possible to omit the type constructors f, g in class StructureConversion f g - especially not in the case of FunctionalDependencies.

  • Should FlexibleContexts and FlexibleInstances should be possible?
@pthariensflame

This comment has been minimized.

Show comment
Hide comment
@pthariensflame

pthariensflame Sep 22, 2016

Contributor

I'd like to note that Rust already has multi-parameter type classes, under the name "traits with (additional) type parameters."

Contributor

pthariensflame commented Sep 22, 2016

I'd like to note that Rust already has multi-parameter type classes, under the name "traits with (additional) type parameters."

@Centril

This comment has been minimized.

Show comment
Hide comment
@Centril

Centril Sep 22, 2016

Contributor

@pthariensflame I'm guessing you are referring to traits such as:

trait ConvertTo<Output> {
    fn convert(&self) -> Output;
}

https://doc.rust-lang.org/book/traits.html#where-clause

While this is true, Output has in this case kind *, which is different than multi-parameter type classes with HKTs.

Contributor

Centril commented Sep 22, 2016

@pthariensflame I'm guessing you are referring to traits such as:

trait ConvertTo<Output> {
    fn convert(&self) -> Output;
}

https://doc.rust-lang.org/book/traits.html#where-clause

While this is true, Output has in this case kind *, which is different than multi-parameter type classes with HKTs.

@eddyb

This comment has been minimized.

Show comment
Hide comment
@eddyb

eddyb Sep 22, 2016

Member

@Centril What's different between Self and Output having a kind other than type, syntax aside?

Member

eddyb commented Sep 22, 2016

@Centril What's different between Self and Output having a kind other than type, syntax aside?

@Centril

This comment has been minimized.

Show comment
Hide comment
@Centril

Centril Sep 22, 2016

Contributor

@eddyb
Nothing I guess. You could allow for Output<T> to make Output a type constructor of kind * -> *, just like for Self<T>. Then:

class StructureConversion f g where
    convert :: f a -> g a

would become (using @burdges syntax):

trait StructureConversion<Output> {
    fn convert<A>(&self: Self<A>) -> Output<A>;
}

Functional dependencies would be covered by associated types, so:

class Monad m => MonadState s m | m -> s where
    get   :: m s
    put   :: s -> m ()
    state :: (s -> (a, s)) -> m a

becomes:

trait MonadState: Monad<Self> {
    type State;
    fn get() -> Self<Self::State>;
    fn put(state: Self::State) -> Self<()>;
    fn state<A, F>(f: F) -> Self<A>
        where F: FnOnce(Self::State) -> Self<(A, Self::State)>;
}
Contributor

Centril commented Sep 22, 2016

@eddyb
Nothing I guess. You could allow for Output<T> to make Output a type constructor of kind * -> *, just like for Self<T>. Then:

class StructureConversion f g where
    convert :: f a -> g a

would become (using @burdges syntax):

trait StructureConversion<Output> {
    fn convert<A>(&self: Self<A>) -> Output<A>;
}

Functional dependencies would be covered by associated types, so:

class Monad m => MonadState s m | m -> s where
    get   :: m s
    put   :: s -> m ()
    state :: (s -> (a, s)) -> m a

becomes:

trait MonadState: Monad<Self> {
    type State;
    fn get() -> Self<Self::State>;
    fn put(state: Self::State) -> Self<()>;
    fn state<A, F>(f: F) -> Self<A>
        where F: FnOnce(Self::State) -> Self<(A, Self::State)>;
}
@mckeankylej

This comment has been minimized.

Show comment
Hide comment
@mckeankylej

mckeankylej Sep 25, 2016

Absolutely no MPTCs. They are an abomination to work with in the internals of GHC and in practice. They make infinitely more problems then they are worth (looking at you overlapping instances and the many solutions to them). Keep rust free of MPTC and instead focus on improving associated types or in Haskell terms type families.

mckeankylej commented Sep 25, 2016

Absolutely no MPTCs. They are an abomination to work with in the internals of GHC and in practice. They make infinitely more problems then they are worth (looking at you overlapping instances and the many solutions to them). Keep rust free of MPTC and instead focus on improving associated types or in Haskell terms type families.

@withoutboats

This comment has been minimized.

Show comment
Hide comment
@withoutboats

withoutboats Sep 25, 2016

Contributor

@mckeankylej I'm confused by your comment.

You're using very specific Haskell jargon, so I'm not sure I actually understand what feature you're talking about. But I looked up "MPTC" and found out it stood for "multi-parameter type class," or, in Rust terms, a trait with type parameters in addition to the Self type. If this is the feature you mean, I am very confused, because:

  1. Rust has traits with multiple type parameters, and has had them for years - since before it became stable and before it gained associated types. Most of the operator traits are multi-parameter, for example.
  2. I don't know what multi-parameter traits really have to do with higher kinded polymorphism. If anything the issue is multi-parameter type constructors, because it introduces a requirement for either partial application or currying.
  3. In Rust, at least, impls of a trait with only a single parameter can easily overlap, multi-parameter traits are not the cause of this issue.
Contributor

withoutboats commented Sep 25, 2016

@mckeankylej I'm confused by your comment.

You're using very specific Haskell jargon, so I'm not sure I actually understand what feature you're talking about. But I looked up "MPTC" and found out it stood for "multi-parameter type class," or, in Rust terms, a trait with type parameters in addition to the Self type. If this is the feature you mean, I am very confused, because:

  1. Rust has traits with multiple type parameters, and has had them for years - since before it became stable and before it gained associated types. Most of the operator traits are multi-parameter, for example.
  2. I don't know what multi-parameter traits really have to do with higher kinded polymorphism. If anything the issue is multi-parameter type constructors, because it introduces a requirement for either partial application or currying.
  3. In Rust, at least, impls of a trait with only a single parameter can easily overlap, multi-parameter traits are not the cause of this issue.
@BardurArantsson

This comment has been minimized.

Show comment
Hide comment
@BardurArantsson

BardurArantsson 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)

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.

Show comment
Hide comment
@withoutboats

withoutboats Sep 25, 2016

Contributor

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.

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.

Show comment
Hide comment
@Centril

Centril Sep 25, 2016

Contributor

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

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.

Show comment
Hide comment
@JohnColanduoni

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

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.

Show comment
Hide comment
@ubsan

ubsan Oct 8, 2016

Contributor

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

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.

Show comment
Hide comment
@brendanzab

brendanzab Oct 12, 2016

Member

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... :/

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.

Show comment
Hide comment
@skeet70

skeet70 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

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.

Show comment
Hide comment
@larroy

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

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.

Show comment
Hide comment
@Boscop

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

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.

Show comment
Hide comment
@withoutboats

withoutboats Oct 27, 2016

Contributor

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.

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.

Show comment
Hide comment
@eddyb

eddyb Oct 27, 2016

Member

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

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.

Show comment
Hide comment
@jmegaffin

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

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.

Show comment
Hide comment
@withoutboats

withoutboats Dec 28, 2016

Contributor

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

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

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.

Show comment
Hide comment
@pthariensflame

pthariensflame Dec 29, 2016

Contributor

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

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.

Show comment
Hide comment
@jmegaffin

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

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.

Show comment
Hide comment
@glaebhoerl

glaebhoerl Dec 29, 2016

Contributor

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

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.

Show comment
Hide comment
@jmegaffin

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

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.

Show comment
Hide comment
@withoutboats

withoutboats Jan 3, 2017

Contributor

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.

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.

Show comment
Hide comment
@glaebhoerl

glaebhoerl Jan 4, 2017

Contributor

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

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.

Show comment
Hide comment
@DemiMarie

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

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.

Show comment
Hide comment
@tioover

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment