fsharp / fslang-suggestions

The place to make suggestions, discuss and vote on F# language and core library features
346 stars 21 forks source link

Support type classes or implicits #243

Open baronfel opened 8 years ago

baronfel commented 8 years ago

NOTE: Current response by @dsyme is here: https://github.com/fsharp/fslang-suggestions/issues/243#issuecomment-916079347


Submitted by exercitus vir on 4/12/2014 12:00:00 AM
392 votes on UserVoice prior to migration

(Updated the suggestion to "type classes or implicits", and edited it) Please add support for type classes or implicits. Currently, it's possible to hack type classes into F# using statically resolved type parameters and operators, but it is really ugly and not easily extensible. I'd like to see something similar to an interface declaration:

class Mappable = 
    abstract map : ('a -> 'b) -> 'm<'a> -> 'm<'b>

Existing types could then be made instances of a type classes by writing them as type extensions:

type Seq with
class Mappable with
    member map = Seq.map

type Option with
class Mappable with
    member map = Option.map

I know that the 'class' keyword could be confusing for OO-folks but I could not come up with a better keyword for a type class but since 'class' is not used in F# anyway, this is probably less of a problem.

Original UserVoice Submission Archived Uservoice Comments

kurtschelfthout commented 7 years ago

Some thoughts on the concerns...

Design complexity and mistakes? - Yes, especially interaction with existing features (subtyping, units of measure, existing kinds of types, quotations, reflection, type providers, whatever)

Yes, this is tricky. I think it can be overcome with sufficient attention/time though. (But that is probably a problem in and of itself...)

Anyway, one thought I had on this is that witnesses resolution is similar to method overloading and thus we can apply the same principles:

As for reflection/quotations/type providers - one advantage of the encoding in the prototype is that it's all done using existing types and very little information is lost. So for reflection for example, they can be represented as such. Similarly, for quotations a call to a trait method can be desugared to the call on the witness. There is precedent for that in quotations, e.g. pattern matching.

Just a few ideas without claiming any originality or great insights, definitely dragons in this area...

Impact on the ecosystem? - Yes, I'm concerned about this. Complexification? - Yes, very, very, very concerned about this. See https://wiki.haskell.org/Typeclassopedia. We don't seriously want all this kind of thinking to be prevalent and common in the F# ecosystem

I think impact on the ecosystem is frightening because it's very hard to predict. On the other hand, it doesn't move as fast as people like to think.

That link is one extreme, but e.g. Swift and Rust (and Clojure) have trait-like facilities and everything seems very much under control. This feature can be put to good and pragmatic use. It would be interesting to see how Ocaml does if they'd add something like modular implicits. Note that the languages where you could argue things have become complex have many, many more features overall.

Feature creep? Yes, I'm very concerned by this. See the regular demands for HKT. Then Type Families. Then GADTs. Then ....

Respectfully: https://yourlogicalfallacyis.com/slippery-slope

These features are not the "simple" kind that someone is going to implement in their spare time and they'll just pop up as a PR to the F# repo. Basically I bet no one at the time of this writing has the necessary time, inclination and knowledge to even start on a prototype of any of the features mentioned (including traits) without getting a lot of @dsyme 's support and time. In this case, restraint is not only possible, it's the overwhelmingly likely outcome.

gusty commented 7 years ago

@kurtschelfthout Regarding SRTPs, it's not clear to me if this is a limitation:

the biggest problem I have with this approach though is that you effectively can't write recursive functions with testables because of the inline. So if you're thinking of writing an andAll : [testable] -> testable by recursively calling and, forget about it.

I'm not sure what do you mean, I know you can't declare a function with both inline and rec keywords at the same time, but that's very easy to workaround, you just do the rec in an inner function.

kurtschelfthout commented 7 years ago

@gusty

I'm not sure what do you mean, I know you can't declare a function with both inline and rec keywords at the same time, but that's very easy to workaround, you just do the rec in an inner function.

I understand. I shouldn't have implied it was impossible. I do think it's one of the many examples with the SRTP approach where unnecessary "ceremony" is required. And this one is more painful imo than the stuff required at declaration site, because it becomes apparent to users of the library.

It's true overall though that technically speaking SRTPs can be used to deal with the requirements of the example. Perhaps a way "around" this suggestion (and others....) is to improve the syntax/tooling/semantics etc around SRTPs, there is already some work in that direction like your compiler speed improvements and the accepted proposal to simplifying member call syntax.

gusty commented 7 years ago

@kurtschelfthout

It's true overall though that technically speaking SRTPs can be used to deal with the requirements of the example. Perhaps a way "around" this suggestion (and others....) is to improve the syntax/tooling/semantics etc around SRTPs, there is already some work in that direction like your compiler speed improvements and the accepted proposal to simplifying member call syntax.

I agree, this is something that needs to be considered. I mean, typeclasses would present another way to achieve the same but with completely different syntax. Although technically different (since it will use dictionary passing instead of inlining) but in the end serves the same purposes.

If you think about it, each time you have a member constraint you have something like a single method typeclass.

This is an open question: Why do we need to maintain two different features, is there a way to unify them?

kurtschelfthout commented 7 years ago

@gusty

If you think about it, each time you have a member constraint you have something like a single method typeclass. This is an open question: Why do we need to maintain two different features, is there a way to unify them?

Because ease of use, documentation and discoverability are critical to gain any traction. One could also ask why do we need union types and record types when we have classes? Functions when we have static methods? Pattern matching when we have switch statements? F# when we have C#? ;-)

Ultimately, putting features in a language makes them part of what programmers can reasonably be expected to understand, and what tools (like documentation tools, IDEs etc) are expected to support. As of now, the SRTP typeclass mechanism is basically an encoding used in a few libraries...no offense!

To get a flavour of what I mean, compare the documentation for FSharpPlus' Abs and Abs' - why is there an Abs'? - with say Swift's AboluteValuable. (start dramatisation) After 10+ years of F# coding I'm not sure how to add my type to the Abs "typeclass". But after a few hours of Swift documentation reading I have a pretty good idea how to make my Swift type conform to the AbsoluteValuable protocol. (end dramatisation)

If you see a way of getting SRTPs to that place then by all means argue for it and suggest ideas. Right now, I am still pretty much convinced that the traits proposal (or family of proposals :) ) in this suggestion - while being a new language feature that will have some overlap with what you can do with SRTPs - has a much better chance of success.

I do absolutely agree that interplay between traits (or any new feature, really) and existing features needs to be considered carefully. In particular for traits that's probably one of the major challenges because there are already quite a few features that are very much in the ballpark (method overloading, comparison/equality, SRTP, interfaces, reflection, ...).

gusty commented 7 years ago

@kurtschelfthout I see what you mean and again I mostly agree with you. But let me point out that F#+ is not a good example (to compare with Swift) since it's still not 1.0 and the doc is work in progress at the moment.

And those poor names reflect my temporary lack of creativity to come up with better names or to organize them in different Namespaces, there's still an open issue for all those numeric abstractions.

Also be aware that compared to similar efforts in other languages (Scalaz, Haskell libs, Purescript libs, etc) I'm almost alone (though there were some interesting collaborations which I appreciate a lot) and the universe of abstractions is quite big.

Having said that I think F#+ is definitely a good example of what kind of libraries and abstractions will arise once we have a feature like this in place. And in fact those design issues will remain, even in Haskell there are many discussions and nobody is happy with their numeric abstractions.

So, this is another interesting discussion: Once we have this feature implemented, what standard abstractions are we going to adopt? See all the design issues and open questions I have in F#+. Most of them would still apply.

Actually we can branch F#+ and re-implement it with the F# compiler's typeclasses experimental branch, that will be really interesting.

You can say "no, we will focus instead on lightweight abstractions, without too much Cathegory Theory behind it" but then I wonder if SRTP is better suited for those kind of Ad-Hoc abstractions.

So, more open questions than answers, I know ;)

kurtschelfthout commented 7 years ago

To make it a bit easier for people to play with traits in F#, I have updated the traits branch again, as well as added some instructions to get going.

https://github.com/kurtschelfthout/visualfsharp/tree/traits

dsyme commented 7 years ago

@kurtschelfthout Thanks for doing that! cc @MattWindsor91

MattWindsor91 commented 7 years ago

Thanks a lot for doing this. Sorry I've been silent recently—in the last year of my PhD now, so I haven't really been able to page in type classes =(

It's nice to see that there's a lot of interest and a few more case studies for this, though!

robkuz commented 7 years ago

I am pretty amazed by @gusty 's comments. I have never thought along these lines but I see the point.

From my POV there are 3 aspects that needs to be adressed

Discussion

module Classes =
    type LalaClass< ^T when ^T: (static member ToLala: ^T -> string)> = ^T
    let inline toLala(t:LalaClass< ^T>) = (^T:(static member ToLala: ^T -> string) t)

module Bundling =
    open Classes
    type Bar = {b: bool}
        with
        static member ToLala(x: Bar) = "lala!"

    let bar = {b = true}
    let l = toLala bar

that is what you can do already with ZERO changes to F# today. The only thing is that the SRTP part of the compiler does not pick up extension methods. If it would, we could have fully functional type classes immediately.

module Classes =
    type LalaClass< ^T when ^T: (static member ToLala: ^T -> string)> = ^T
    //using the hopefully new syntax here
    let inline toLala(t:LalaClass< ^T>) = ^T.ToLala t

module Types =
    type Foo = {s: string}

module Bundling =
    open Classes
    open Types

    type Foo with
        static member ToLala(x: Foo) = "lala?"

    let foo = {s = "foo"}
    toLala foo
    //     ^^^---- this will compile time err as
    //             the compiler does not see the 
    //             extension method for type Foo
    //             but this should really just compile

And we were really bold we could so something like this

module Classes =
    type LalaClass< ^T when ^T: (static member ToLala: ^T -> string)> = ^T
    let inline toLala(t:LalaClass< ^T>) = ^T.ToLala t

module Types =
    type Foo = {s: string}

module Instances =
    open Types
    type Foo with
        static member ToLala(x: Foo) = "lala?"

module Bundling =
    open Classes
    open Types
    open Instances

    type Bar = {b: bool} with
        static member ToLala(x: Bar) = "lala!"

    let bar = {b = true}
    let l = toLala bar

    let foo = {s = "foo"}
    toLala foo

There are some SO-questions adressing this 1, 2, 3

For me this feels much more like a bug then a feature. @Dsyme explains the reasons here. Thou I don't know how much of reasons given then (3.0 & 3.1) is still true as quite some time has been invested into the compiler meanwhile (AFAIU)

Postscript

Having said all this - I am not opposed to the trait proposal at all. I'd rather have the traits in then no way to do type classes. However extending SRTPs might be a clearer and faster way. Just sayin'

realvictorprm commented 7 years ago

@kurtschelfthout Thanks for usage explanation. It was really helpful and well, I think this is a useful feature 🤔

kurtschelfthout commented 7 years ago

@robkuz Let's be realistic though, SRTPs have much further to go than your toy example implies. Show me a trait with multiple methods, in a trait hierarchy, with default implementations, where one of the trait methods is overloaded by return type only (e.g. member pi : 'a), and a witness with a generic type argument where the witness implementation requires a trait constraint. What happens when there is a trait defined in library A, and then you have multiple witnesses for the same type (say in project B and C, but doesn't really matter) how do you disambiguate?

Note all that works in the current trait prototype and already looks relatively nice and intuitive to me, e.g. see the examples: https://github.com/kurtschelfthout/visualfsharp/tree/traits/examples

Not saying it's ready to go and all problems are solved, far from it, but imo much closer than SRTPs. The syntactic improvement you mention - also note that only covers instance member SRTP calls - is really an insignificant step in comparison to where it needs to be.

robkuz commented 7 years ago

@kurtschelfthout sure SRTPs are limited and I wouldn't know how to express hierarchies or default implementations (multiple methods thou are possible). In anyway I am not opposed against Traits. Not at all. The better/stronger the features the better. The only thing is that I am worried that this Trait proposal will never happen or maybe only when C# has implemented it (bc interop etc.) and meanwhile we struggle to express certain kinds of abstractions that are hard to express at the moment with F# (as you have laid out in your own examples about FSCheck).

Alxandr commented 7 years ago

I mostly like the trait proposal as it stands currently, I would just like to take into consideration that while it might not be for the first version, it would likely be a good idea to think about some way to extend/improve whatever construct we decide to go for to something that is able to express fmap and/or bind eventually.

robkuz commented 7 years ago

One question about the actual implementation (I don't have it running on my machine) - Do SRTPs pick up methods defined in Traits?

jindraivanek commented 7 years ago

@robkuz Tried it, but without success: https://gist.github.com/jindraivanek/4d461800cfc9d1bf33507c783aec5712. But maybe my SRTP-fu is too low :)

kurtschelfthout commented 7 years ago

@robkuz @jindraivanek No they won't. Because (somewhat similar to methods defined in extensions) the methods are not defined on the actual class. To make this somewhat clearer, the type of something like Show.show in @jindraivanek's example (which is how you would call the trait, e.g. Show.show 1 or Show.show 1.0) is:

> Show.show;;
val it : ('a -> string) when 'a implies 'b and 'b :> Show<'a> and 'b : struct

This reveals the implementation strategy: beside the actual value of type 'a that you pass in, the method has an additional parameter 'b which represents the witness - and the compiler allows you to omit the witness type because it is implied by 'a. Essentially what the compiler does is track witnesses in scope and compiles Show.show 1 to something like defaultof<ShowInt>.show 1 because it figured out that ShowInt is the type that implements Show<int> and you're passing in an int.

(Note that to make member constraints work with extension methods the compiler would have to do a similar thing as tracking witnesses - track extension members in scope).

All that said - I think since the compiler now passes the witnesses around in a lot of places, perhaps it is not too hard to add resolution of member constraints to witnesses. Not convinced it's desirable though.

robkuz commented 7 years ago

@jindraivanek thanks for trying this out. @kurtschelfthout Thanks for the explanation. As for the "desirability" ... give me a release of this for a month and I will come back with at least 2 instances where it will be ;-)

A question to the SRTP that you are showing. Can the implies part be written verbatim? or is that only available in the fsi?

robkuz commented 7 years ago

Here is some code by @jindraivanek that stresses the actuall type class implementation. Can anybody explain why this fails? https://gist.github.com/jindraivanek/e217d9352fa67f1adf9dedd441c45154

voronoipotato commented 7 years ago

This might be a bit late in response @dsyme but I didn't see any similar arguments. Your language already has all the complexity of Typeclassopedia, you just don't have explicit names for them defined in the language. Your language has functors, applicatives, monads, and monoids. So to say you don't want that level of complexity is really saying you don't want people talking about the complexity which is already there. What is a fair argument is that you should not be required to speak with this level of preciseness but it seems strange to bar people from being able to.

MattWindsor91 commented 7 years ago

(BTW, I'm now back interning with @crusso - it may be possible that I'll be diving back into F# traits, but it depends on what the lie of the land is. I need to catch up with this discussion first!)

EDIT: Just to clarify, this is neither definite nor official, just a possibility at this stage. I just wanted to ping to say that I'm still interested and back in the wider area.

TobyShaw commented 7 years ago

So, I wanted to play around and try implement some limited form of this via SRTP working with type extensions. My strategy was to pass the NameResolutionEnv to the point at which GetRelevantMethodsForTrait was called such that the ExtensionMethods could be found through there.

My problem was that the type variables did not align in the way I hoped. While I could fully view the relevant ExtensionMethods of the correct name at the correct point of execution, naively passing them along to the constraint solver didn't give the desired behaviour (duh!).

Could anyone with more knowledge of this area of the codebase give me some hints as to where to go from here? Even if not accepted into the codebase I'd love to understand the compiler a bit more in this regard.

dsyme commented 7 years ago

@TobyShaw If you look at the implementation in https://github.com/kurtschelfthout/visualfsharp/tree/traits/ then it may give you some inspiration about how to propagate the "solutions" to constraints through the constraint solver etc. These are the solution to type class constraints, though I think you could use much the same technique for the solutions to SRTP trait constraints

dsyme commented 7 years ago

@TobyShaw Actually, looking again it wasn't quite as clear as I remembered :) Some notes

TobyShaw commented 7 years ago

Awesome, thanks very much!

I'm sure this sort of thing has been done loads of times before, still fun nonetheless.

dsyme commented 7 years ago

@TobyShaw No, it hasn't - and I've been meaning to do exactly this for some time now :) Let me know if you get anywhere

dsyme commented 7 years ago

See also this: https://github.com/fsharp/fslang-suggestions/issues/230

zpodlovics commented 7 years ago

Well, it seems I managed to a get working example rewriting @jindraivanek gist. The idea was to find an expression where the Unchecked.defaultof<MyGenericType<_>> instantiation is a value type. It works well with reference types too. Update: Unfortunately it only works for ToString...

F# Interactive:

> typeof<Show<int>>.IsValueType;;      
val it : bool = false
> typeof<StructShow<int>>.IsValueType;;
val it : bool = true
> Unchecked.defaultof<Show<int>>;;      
val it : Show<int> = null
> Unchecked.defaultof<StructShow<int>>;;
[<Struct>]
val it : StructShow<int> = StructShow

Example:

    open System

    type Show<'a> = 
        abstract member show: 'a -> string

    [<Struct>] 
    type StructShow<'a> =
        member __.show x = x.ToString()

    [<Struct>]
    type MyStruct =
        override __.ToString() = "MyStruct.ToString()"

    type MyClass() =
        override __.ToString() = "MyClass.ToString()"

    let inline show'< ^w, ^a when ^w : (member show : ^a -> string)> (w: 'w, a: 'a) =
        (^w: (member show : 'a -> string) Unchecked.defaultof<'w>, a)
    let inline show (x: 'T) = show'(Unchecked.defaultof<StructShow<_>>, x)

    show 1 |> printfn "%s"
    show 1.1 |> printfn "%s"
    show (MyStruct()) |> printfn "%s"
    show (MyClass()) |> printfn "%s"
1
1.1
MyStruct.ToString()
MyClass.ToString()
TobyShaw commented 7 years ago

Sadly, I didn't have much luck taking your approach @dsyme, I'm not sure I have a good enough mental model of the constraint solving code to get anywhere with it.

Could you comment on the likely effectiveness of the approach I suggested last time? I got the impression you didn't think it was going to be an effective solution, but I'm not sure why that is.

I have the NameResolutionEnv in scope at the point in which I call GetRelevantMethodsForTrait, so while it currently only searches through intrinsic methods, it ought to be possible to adapt it to search through the extensions members stored in the name resolution env. The only problem is that the types attached to the extension members won't match the types stored in the trait.

I need to be updating the types stored in the NRE any time I freshen type variables in other constraints, otherwise they'll be out of date by the time I want to search through it.

So the solution is just to: Update NRE in step with when I'm updating typars (not sure how easy this is) Pass the NRE to GetRelevantMethodsForTrait, and search it for a trait which matches in name and types. (this is easy)

TobyShaw commented 7 years ago

After posting this, I realised how what you were suggesting would work.

It's effectively the same idea is what I proposed, you store the extensions members in the constraint rather than in the NRE (the NRE is used to populate the constraints). This way, the algorithm for freshening type variables needs less modification, since it's just one type of constraint that needs changing, rather than a whole new thing (the NRE).

Thanks for being a rubber duck, I guess :) I'll give it another go tomorrow.

TobyShaw commented 7 years ago

So, small update:

Using this test program:

type System.Int32 with static member TestMethod(a : System.Int32, b : System.Int32) = a + b

type System.Boolean with static member TestMethod(a : System.Boolean, b : System.Boolean) = a && b

type MyType = | MyType of int static member TestMethod(MyType(a),MyType(b)) = MyType(a + b)

let inline myTestMethod< ^A when ^A : (static member TestMethod : ^A * ^A -> ^A) > (a : ^A) (b : ^A) =
    ( ^A : (static member TestMethod : ^A * ^A -> ^A) (a,b) )

[<EntryPoint>]
let main args = ignore (myTestMethod true false); 0

Seems like I'm able to select the correct extension method.

image

However, despite being recorded as I hoped, it still gives an error message of:

"fsharptest.fs(11,25): error FS0193: The type 'Microsoft.FSharp.Core.bool' does not support the operator 'TestMethod'"

May have to take a break for today, but any thoughts on this would be much appreciated.

TobyShaw commented 7 years ago

@dsyme I lied, didn't take a break. Got it compiling as expected (though it's definitely not a clean solution at the moment).

Haven't checked whether the resulting IL generated works as expected, although I can't see why it wouldn't.

https://github.com/Microsoft/visualfsharp/pull/3582

robkuz commented 7 years ago

@TobyShaw does that already work with generic and recursive types as well? so that this would work

let inline show< ^A when ^A : (static member show : ^A  -> string) > (a : ^A) =
    ( ^A : (static member Show : ^A -> string) a )

type System.String with static member Show(a : System.String) = sprintf "{String: %A}" a

type System.Boolean with static member Show(a : System.Boolean) = sprintf "{Boolean: %A}" a

type MyType<'a> = | MyType of 'a 

type MyType<'a> = with
    static member Show(v: MyType<'a>) = 
        match v with
        | MyType a -> sprintf "{MyType: %A}" (show a)

[<EntryPoint>]
let main args = 
    let o = show "myself"               // {String: myself}
    let p = show true                   // {Boolean: true}
    let q = show 1                      // compiler error as no Int.Show in place
    let r = show (MyType "my way")      // {MyType: {String: my way}}
    let s = show (MyType 1)             // compiler error as no Int.Show in place
    let s = show (MyType (MyType true)) // {MyType: {MyType: {String: true}}}

    0
TobyShaw commented 7 years ago

So, you currently can't write that extension member on MyType in F#, it won't compile. Since for some reason, extension members can't have constrained type parameters on.

My patch does not address this, but I agree it would be a necessary addition in order for this to fully recreate the benefits of typeclasses.

Alxandr commented 7 years ago

The problem with this is that there is no type safety in static member Show(v: MyType<'a>) =. This can result in major headaches because of you having misspelled something because you can't state the contract you're trying to fulfill.

robkuz commented 7 years ago

Why would that be?

type MyType<'A> = with
    static member Show< ^B when ^B : (static member show : ^B  -> string) >(v: MyType< ^B >) = 
        match v with
        | MyType a -> sprintf "{MyType: %A}" (show a)

That should do the trick (disregarding that fact that the way to state this is really awful)

robkuz commented 7 years ago

@Alxandr also this works out of the box today (F# 4.1) if the method is defined directly on the type (and not as an extension method in another module) and the type inferrer correctly infers that B needs to provide static member Show

    let inline show (a : ^A) = ( ^A : (static member Show : ^A -> string) a )

    type System.String with static member Show(a : System.String) = sprintf "{String: %A}" a

    type System.Boolean with static member Show(a : System.Boolean) = sprintf "{Boolean: %A}" a

    type Stop = 
        | Stop
        with
        static member Show(v: Stop) = "{Stop}"

    type MyType<'A> = 
        | MyType of 'A
        with
        static member inline Show(v: MyType< ^B >) = 
            match v with
            | MyType a -> sprintf "{MyType: %A}" (show a)

    let p = show (MyType Stop)
    let s = show (MyType (MyType Stop))
    let s = show (MyType (MyType "foo")) // error as extension methods are not picked up atm

If this was not type safe and you could misspell something the whole effort would be in vain, wouldn't it?

TobyShaw commented 7 years ago

Ahh, without the inline it doesn't compile. I admit I did not know about inline static members, learned something new today.

Alxandr commented 7 years ago

On phone, so I apologize for bad reply. You misunderstood me, @robkuz, or rather I wrote it in a bad way. The problem is that there is no good way to state that you want this static method to implement the Show protocol/trait/typeclass. This means it could be refactored away for instance without anyone being the wiser until you compile. And you get errors at callsites, rather than where you make the class.

robkuz commented 7 years ago

@Alxandr I agree, it would be nice if we could explictly state intention here and I agree in the absence of this the error messages at the call site will be pretty surprising especially for newbies.
However the stuff that @TobyShaw is working on is more to extend SRTPs to allow something type-classish. My hope would be that real TCs will be much easier (and more feature rich) than this. But since its not clear when (or for that matter IF) TCs come I am happy to work with allowing extension methods to be picked up by constrained functions

Alxandr commented 7 years ago

@robkuz that I agree with :)

sighoya commented 7 years ago

I would like to see it in F#, +inf

radekm commented 6 years ago

Just want to mention recent paper Familia: Unifying Interfaces, Type Classes, and Family Polymorphism - their ideas may be helpful.

Alxandr commented 6 years ago

I've been doing some experiments using SRTP and "shapes" (more or less as defined by the C# proposal). The end result seems to be pretty decent, though you won't be able to encode monads ofc. I still need to figure out a good way to encode shapes that extend/implement other shapes, but besides from that this seems to be both working and producing fairly efficient code:

[<Struct>]
type Proxy<'t> = 
  member __.Type = typeof<'t>

module Proxy =
  let inline forType<'t> = Proxy<'t> ()
  let inline forInst<'t> (_: 't) = forType<'t>

module Read =

  type Shape<'t> =
    abstract member read: string -> 't

  let inline shape< ^t, ^impl when ^impl :> Shape< ^t> 
                              and ^impl : struct
                              and ^t: (static member impl_Read: ^impl)> =
    (^t: (static member impl_Read: ^impl) ())

  let inline read s =
    shape.read s

module Show =

  type Shape<'t> =
    abstract member show: 't -> string

  let inline shape< ^t, ^impl when ^impl :> Shape< ^t> 
                              and ^impl : struct
                              and ^t: (static member impl_Show: ^impl)> =
    (^t: (static member impl_Show: ^impl) ())

  let inline show t =
    shape.show t

module Identity =

  // TODO: Figure out how to make Identity require Read and Show := (Read, Show) => Identity
  type Shape<'t> =
    abstract member kind: Proxy<'t> -> string

  let inline shape< ^t, ^impl when ^impl :> Shape< ^t> 
                              and ^impl : struct
                              and ^t: (static member impl_Identity: ^impl)> =
    (^t: (static member impl_Identity: ^impl) ())

  let inline kind p =
    shape.kind p

module SomeModule =

  type Id =
    private
    | Id of string

    static member inline impl_Read = IdReadShape ()
    static member inline impl_Show = IdShowShape ()
    static member inline impl_Identity = IdIdentityShape ()

  and [<Struct>] IdReadShape =
    interface Read.Shape<Id> with
      member __.read s = Id s

  and [<Struct>] IdShowShape =
    interface Show.Shape<Id> with
      member __.show (Id s) = sprintf "Id \"%s\"" s

  and [<Struct>] IdIdentityShape =
    interface Identity.Shape<Id> with
      member __.kind _ = "id-kind"

[<EntryPoint>]
let main _ =
    let test : SomeModule.Id = Read.read "foo"
    let kind = Identity.kind (Proxy.forInst test)
    let str = Show.show test
    printfn "%s (%s)" str kind
    0

[Edit]

Actually, this seems to work for "inheritance":

  let inline shape< ^t, ^impl, ^read, ^show 
                              when ^impl : struct
                              and ^read : struct
                              and ^show : struct
                              and ^impl :> Shape< ^t> 
                              and ^read :> Read.Shape< ^t>
                              and ^show :> Show.Shape< ^t>
                              and ^t: (static member impl_Identity: ^impl)
                              and ^t: (static member impl_Read: ^read)
                              and ^t: (static member impl_Show: ^show)> =
    (^t: (static member impl_Identity: ^impl) ())
Alxandr commented 6 years ago

I tried doing the same using operators, but it seems F# doesn't like multiple SRTP constraints on a method group:

[<Struct>]
type Proxy<'t> = 
  member __.Type = typeof<'t>

module Proxy =
  let inline forType<'t> = Proxy<'t> ()
  let inline forInst<'t> (_: 't) = forType<'t>

module Read =
  type [<Struct>] Tag =
    static member name = "Read"

  type Shape<'t> =
    abstract member read: string -> 't

  let inline shape< ^t, ^impl when ^impl :> Shape< ^t> 
                              and ^impl : struct
                              and ^t: (static member (~~): Tag -> ^impl)> =
    (^t: (static member (~~): Tag -> ^impl) (Tag ()))

  let inline read s =
    shape.read s

module Show =
  type [<Struct>] Tag =
    static member name = "Show"

  type Shape<'t> =
    abstract member show: 't -> string

  let inline shape< ^t, ^impl when ^impl :> Shape< ^t> 
                              and ^impl : struct
                              and ^t: (static member (~~): Tag -> ^impl)> =
    (^t: (static member (~~): Tag -> ^impl) (Tag ()))

  let inline show t =
    shape.show t

module Identity =
  type [<Struct>] Tag =
    static member name = "Identity"

  type Shape<'t> =
    abstract member kind: Proxy<'t> -> string

  let inline shape< ^t, ^impl(*, ^read, ^show *)
                              when ^impl : struct
                              //and ^read : struct
                              //and ^show : struct
                              and ^impl :> Shape< ^t> 
                              //and ^read :> Read.Shape< ^t>
                              //and ^show :> Show.Shape< ^t>
                              and ^t: (static member (~~): Tag -> ^impl)
                              (*and ^t: (static member (~~): Read.Tag -> ^read)
                              and ^t: (static member (~~): Show.Tag -> ^show)*)> =
    (^t: (static member (~~): Tag -> ^impl) (Tag ()))

  let inline kind p =
    shape.kind p

module SomeModule =

  type Id =
    private
    | Id of string

    static member inline (~~) (_: Read.Tag) = IdReadShape ()
    static member inline (~~) (_: Show.Tag) = IdShowShape ()
    static member inline (~~) (_: Identity.Tag) = IdIdentityShape ()

  and [<Struct>] IdReadShape =
    interface Read.Shape<Id> with
      member __.read s = Id s

  and [<Struct>] IdShowShape =
    interface Show.Shape<Id> with
      member __.show (Id s) = sprintf "Id \"%s\"" s

  and [<Struct>] IdIdentityShape =
    interface Identity.Shape<Id> with
      member __.kind _ = "id-kind"

[<EntryPoint>]
let main _ =
    let test : SomeModule.Id = Read.read "foo"
    let kind = Identity.kind (Proxy.forInst test)
    let str = Show.show test
    printfn "%s (%s)" str kind
    0

So it seems it's either nice-er syntax or ability to define hierarchies for now... Also, the hierarchies will quickly stop working as you need to include all of the parents (transitively).

gsomix commented 6 years ago

Typeclass Traits proposal for Dotty https://github.com/lampepfl/dotty/pull/4153

realvictorprm commented 6 years ago

I'll focus in the next months on helping to get PR's merge ready which address problems and features for SRTP's.

zpodlovics commented 6 years ago

It seems that interface default methods is coming to dotnet (note: clr runtime changes required): https://github.com/dotnet/csharplang/issues/52

Why it's important? It may help to support traits. Example: Scala traits are implemented as interface default methods: https://github.com/scala/scala-dev/issues/35

However if SRTP encoding is better, than that should be the default choice.

ghost commented 6 years ago

I just watched The F# Path To Relaxation talk given by @dsyme at NDC Oslo 2018 and type classes as well as HKT was mentioned as something we don't do in F# partly because of implementation issues and partly because of not having complete control of the .NET library-design.

Is this final? I would personally love to see these features make it into the language some day..

MaxWilson commented 6 years ago

@erbaman, I have no inside knowledge, but it seems to me that it just means you need to do type classes in such a way that you can still benefit from them on types that you don't control. The WitnessAttribute approach discussed above would let you assign .NET library types to type classes as necessary, so I think that concern could go away.

I could be wrong, but the way I interpreted Don's talk was more of a "here's why type classes are less valuable/more work in F# than in Haskell" than a veto. Even if you DID add WitnessAttribute support to the language and the compiler, you'd still need someone to do the work of creating appropriate type classes and witnesses for them, and so far no one has.