-
Notifications
You must be signed in to change notification settings - Fork 0
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 Types Proposal #1
Comments
I realize HKT is going to be a very large undertaking and a significant change to the language. So, I'd like to kick off the higher-Kinded types proposal drafting process with this issue. The top comment is the proposal itself, and the comments will be reserved for discussion of current drafts. By the end, we will submit the complete proposal to swift-evolution for community review. |
Protocols being the most obvious place to start with new stuff, here's a couple versions of our Functor with HKTs: Version 1: Attack of the angle brackets. protocol Functor<Self<_>> {
typealias A
func fmap<B>(f : A -> B) -> Self<B>
} Version 2.1: Where clauses with angles protocol Functor where Self<_> {
typealias A
func fmap<B>(f : A -> B) -> Self<B>
} Version 2.2: Where clauses with no parameter binding protocol Functor where Self : _ -> _ {
typealias A
func fmap<B>(f : A -> B) -> Self<B>
} Version 2.3: Where clauses with parameter binding protocol Functor where Self : A -> _ {
func fmap<B>(f : A -> B) -> Self<B>
} |
I lean towards the last of these (because I hate associated types with a passion, but I recognize it requires the most change and a significant amount of syntax. |
Can't we regard v2.3 as sugar for v2.2? |
I wanted to make a distinction between typealiases and type parameters. I don't want swift to start dumping implicit typealiases all over the place - tho it would be a very swift-y thing to do. I was thinking 2.3 would bring the parameter into scope without introducing a new type into the protocol. |
Actually, how about: v1.2: Angles in the outfield protocol<A> Functor {
func fmap<B>(f : A -> B) -> Self<B>
} |
That would also allow us to write such things as: // ignore Functor, etc. dependencies for this demo
protocol<A> Monad {
static func unit() -> Self<A>
func bind<B>(f : A -> Self<B>) -> Self<B>
}
protocol<M : Monad, A> MonadTrans {
init(lifting: M<A>) -> Self<M, A>
} |
What does conformance look like (say, in an extension)? |
Kind is a constraint on a type parameter, so it makes sense to me to have it be a form that appears after the
I would focus on figuring out the technical work involved before ratholing on syntax too much though. |
I personally prefer @pthariensflame v1.2 without associated types. I think it would be cleaner and easier to work with. |
I agree with @CodaFi in that v2.2/v2.3 would be a next logical step for the language that is addicted to type aliases. |
@jckarter I agree, but wouldn't syntax (and tactictly, features to be added) influence those technical details? |
@CodaFi I imagine conformance working like: extension<A> Array<A>: Functor {
public func fmap<B>(f: A -> B) -> Array<B> { return map(f) }
} EDIT: Or, for short, |
I always thought the way SML was able to encode HKT was the most Swift-like. This requires generic type-aliases, which seems to be a more manageable (and approved by Apple for an implementation) solution. Taken from https://existentialtype.wordpress.com/2011/05/01/of-course-ml-has-monads/ signature MONAD = sig
type 'a monad
val ret : 'a -> 'a monad
val bnd : 'a monad -> ('a -> 'b monad) -> 'b monad
end And converted to Swift protocol MonadOps {
typealias Monad<A>
static func unit<A>(a: A) -> Monad<A>
static func bind<A, B>(monad: Monad<A>, transform: A -> Monad<B>) -> Monad<B>
} |
@adamkuipers That representation, and any others like it, requires explicit dictionary passing. I want to avoid that at nearly any cost, and I'm sure @CodaFi feels similarly. EDIT: Never mind, I misread the proposal. Nonetheless, I don't like it because it's not very “clean” conceptually, and it makes higher kinds second class citizens in the type system. |
The common thread among all of these is that protocols need to start carrying kind information, so I'll start examining what it would take to start doing that. |
I think types in general need to start carrying kind information! Otherwise, how are we going to maintain kind-safety? |
@pthariensflame Sorry, I meant *as user-facing syntax! The compiler can probably do fine inferring kinds for existing generic types. |
Oh, I see. Sorry for the misunderstanding. :) |
Here's a lengthy example that I think better convers the necessary use cases to consider: carrying kind information around, protocol extensions, protocol requirements in functions, protocol inheritance, mapping associated types to higher-kinded protocol parameters, and operators as part of protocol definition. The key insight here is that, in my view, higher-kinded protocols carry the type argument along when conforming to, i.e. (TBH, this is also the weak point here, since it maps badly to existing Swift practice where it's e.g. okay to return So let's define // Higher-kinded protocol with no other constraints.
protocol Functor<A> {
// Inside this definition, `Self<B>` allows exchanging the type parameter.
func fmap<B>(f: A -> B) -> Self<B>
}
// Function definition. `F` is higher-kinded and must be applied with a type.
infix operator <^> { associativity left }
func <^> <F : Functor, A, B>(f: A -> B, x: F<A>) -> F<B> {
return x.fmap(f)
}
// Higher-kinded protocol with inheritance. Repeating `<A>` in the definition
// is necessary, since `A` wouldn't otherwise be visible in the body. (I can't
// make up my mind whether it's required to always write `Self<A>` in the body
// or if the `Self` shortcut should be allowed. But probably shouldn't.)
protocol Applicative<A> : Functor<A> {
static func pure(x: A) -> Self<A>
func <*> <B>(f: Self<A -> B>, x: Self<A>) -> Self<B>
}
extension Array : Functor<Element> {
func fmap<B>(f: Element -> B) -> [B] {
return self.map(f)
}
}
extension Array : Applicative<Element> {
static func pure(x: Element) -> Array {
return [x]
}
}
func <*> <A, B>(fs: [A -> B], xs: [A]) -> [B] {
return fs.flatMap {f in xs.map(f)}
}
extension Array : Monad<Element> {}
func >>- <A, B>(xs: [A], f: A -> [B]) -> [B] {
return xs.flatMap(f)
} Here's a extension Dictionary : Functor<Value> {
func fmap<B>(f: Value -> B) -> [Key: B] {
var result = [Key: B]()
for (k, v) in self {
result[k] = f(v)
}
return result
}
} I can't bend my head into thinking what it would take to support even higher kinds in the syntax, let alone implementing it! But maybe this level of expressivity would be a good start. |
What I don't like about all the proposals that put the higher-kinded |
@pthariensflame If we want to do that, we need to gather a significant amount of evidence that there's real use for it in Swift. Partially applied type operators are quite a foreign concept in Swift today. Support for currying was dropped from function definitions too. |
Use cases and implementation details seem like the biggest stumbling blocks for this proposal followed by syntax. I think that a first use case we should/could provide is making an |
@griotspeak Neither of those are addressed by higher kinds; in fact, the two things you cited are actually concerns for the standard library, too, and Apple is in favor of them (although I can't seem to find where I read that). |
I could easily be mistaken. I was thinking something along the lines of:
EDIT: There is no way–at the language level–that I can see, at present, to express the idea that an |
I'm not completely sure what that would do. As far as I remember, the syntax they were considering for that was something like: extension (SequenceType where Self.Element == Equatable) : Equatable {
…
} EDIT: but I could easily be wrong. |
Compared to the current syntax of: extension SequenceType where Generator.Element : Equatable {
...
} |
Here is a question which should simplify my confusion. Am I mistaken in my understanding that HKT could provide a consistent means to express that an |
They should not (by themselves). The purpose of HKTs is to allow you to express generics parameters that are themselves generic. Here is a good article on them (written for a language which just got them recently). |
@griotspeak I believe this will be addressed by Swift in the next version with complete generics. See: https://github.com/apple/swift-evolution EDIT:
|
Alright then, my mistake. |
@mpurland Ah! You found the reference I was looking for! Thanks! |
@pthariensflame It would be nice to have more clarification on complete generics to inform this discussion further. |
Having a separate address and container value returned from alloc_stack is not really needed in SIL. Even if they differ we have both addresses available during IRGen, because a dealloc_stack is always dominated by the corresponding alloc_stack in the same function. Although this commit quite large, most changes are trivial. The largest non-trivial change is in IRGenSIL. This commit is a NFC regarding the generated code. Even the generated SIL is the same (except removed #0, #1 and @local_storage).
I just had a crazy idea that would simplify this proposal. Instead of depending on getting generic protocols, let's scrap the idea altogether and force root protocols to declare their kind constraints as actual constraints. I propose the following reformulation of the proposal example: // Higher-kinded protocol with no other constraints.
protocol Functor : Self<A> {
func fmap<B>(f: A -> B) -> Self<B> // Self is checked to have kind * -> *
} This way we stop performing kind inference and start performing kind checking which is more in line with one of the original goals of the proposal (fixing a class of programmer error with higher-order structures). For more complex kinds, we support the arrow kind like so /// Ridiculous contrived thing
protocol IFunctorTrans : Self<A, I, M<T>> {
func map<B, J>(trans : A -> B, itrans : I -> J) -> Self<B, J, M<T>>
} Naturally, we must restrict kind constraints to non-derived protocols only to remove the ability to re-kind a protocol. |
That's a really nice solution! I'd definitely +1 it. I wish there were a way to still support protocol inheritance though. Jaden Geller
|
There, I've updated it to make arrows implicit in the structure of generics. That will need to be a separate proposal... |
The trouble is how currying interacts with kind arrows. I'd like to be able to do this in the future: protocol MonadTrans : Self<M<A>> { /* ... */ }
struct StateT<S, M<A>> { /* ... */ }
extension StateT<S, _> : MonadTrans { /* ... */ } But that kind of abstraction makes no sense syntactically. |
Are higher kinded types aka Nested Generics not already supported in Swift 3.1 ? |
@sighoya No. That's Generic Currying. It only gets you some of Higher Kinds. |
@pthariensflame |
Hey guys! Maybe one example of where the lack of higher kinded types is a blocker: there are a lot of different Reactive Stream libraries (RxSwift, ReactiveSwift etc.) but if you would like to create a library that would build on top of them, you would have to choose one and stick to it. So this means that it's now impossible in swift to write code that would work for any Stream. This means, if, potentially you want to write an abstraction over all the Streams, you will have to repeat your code for all of them and (what is even worse and is an actual blocker) you would have to import all of these Reactive Libraries which is just ridiculous. The example of such project: ReactiveReSwift and we were trying to write a Router for it, but had to choose RxSwift because the Router itself uses operations over streams (map, flatMap etc). |
Proposal: Higher Kinded Types in Swift
Introduction
Higher-Kinded Types are an extension to the Swift type system that allows for more expressive generic quantification and protocols that are aware of more of the structure of their conforming types. Kinds, used to great effect in Haskell and Scala, have made generic programming with types easier, richer, and safer in these languages, and have recently attracted the attention of the Rust community because of this.
Motivation
The implementation of lawful structures and protocols in Swift occasionally necessitates being able to ask the Swift type system to check the shape of a type, not just its name. To that end, Higher Kinded Types enable not just a new class of programs to be written safely, but open the doors to better error checking and reporting, and the creation of higher-order structures and patterns.
For example, the standard library implements a number of declarative primitives and combinators, among them
map
. In order to create a model that abstracts over just those types capable of beingmap
ped over, we wrote thisFunctor
protocol:From this signature, it is possible to implement
Functor
in a type-unsafe or unsuitably generic way by mixing up the associated types. In addition, because there was no way to indicate the kind ofFB
, we developed a stack of so-called "Kind Classes" to serve as markers, but the implementor may still overwriteFB
with whatever type they so choose. In addition, any type can claim to be thisFunctor
, meaning that quantification overFunctor
-like things is a fundamentally unsafe operation. This makes our abstraction leaky, fragile, and fundamentally non-composable.With Higher-Kinded Types, that
Functor
protocol can be rewritten as such:NB: Not final syntax, here for demonstration.
This definition uses Kind inference to enforce that any type that wishes to implement
Functor<A>
must have at least the Kind* -> *
to instantiate the parameterA
.Proposed Solution
The compiler must be modified to produce and check kind information for classes, structs, and protocols. In addition, Protocols must now be capable of introducing kind parameters that are reflected in their conforming types. We demonstrate a proposed syntax for these in the next section.
Detailed Design
Syntax
Unlike GHC, our proposal involves implementing Kinds with just 1 possible constructor
*
orKind
for the type of data types. True to Haskell's kind system,*
andk1 -> k2
(wherek1, k2 : *
) will be the only possible constructors forKind
. Kind inference for data types proceeds as a consequence of type checking. For example:NB: NOT FINAL
The most visible user-facing change will be the introduction of parametrized protocols. The syntax for protocol declarations must be amended:
And, though it does not appear in the grammar, the
Self
keyword for parameter types must be extended to allow forgeneric-argument-clause
s in protocol type parameter lists.Errors
New errors that can now be checked as a result:
The text was updated successfully, but these errors were encountered: