3

Support type classes or implicits · Issue #243 · fsharp/fslang-suggestions · Git...

 3 years ago
source link: https://github.com/fsharp/fslang-suggestions/issues/243
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

New issue

Support type classes or implicits #243

baronfel opened this issue on Oct 20, 2016 · 191 comments

Support type classes or implicits #243

baronfel opened this issue on Oct 20, 2016 · 191 comments

Comments

Copy link

Collaborator

baronfel commented on Oct 20, 2016

edited by dsyme

NOTE: Current response by @dsyme is here: #243 (comment)


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

Copy link

Collaborator

cloudRoutine commented on Oct 30, 2016

For those who haven't seen it, there's an experimental implementation of type classes for F#

Hopefully if this is implemented as a language feature the verbose attribute syntax will be dropped in favor of proper keywords.

It's hard to see how

 [<Trait>]
 type Eq<'A> = 
     abstract equal: 'A -> 'A -> bool 

 [<Witness>] // a.k.a instance
 type EqInt = 
      interface Eq<int> with 
        member equal a b = a = b

presents any advantage over a more terse syntax like

trait Eq<'A> = 
    abstract equal: 'A -> 'A -> bool 

witness EqInt of Eq<int> = 
    member equal a b = a = b

which provides both brevity and clarity

Copy link

Collaborator

dsyme commented on Oct 31, 2016

edited

@cloudRoutine There are advantages of a kind.

  • the compiled (and C#-interop) form is considerably more apparent from the first version. You can see that in the above link by the close relationship between existing F# code and the trait-F# version (basically add attributes). It is like looking at the compiled form. This is significant given that Eq probably has good uses as a normal type as well as a class-of-types/trait. It is also significant if you reflect over these, or explicitly instantiate the hidden type parameters
  • it minimizes the actual additions to the F# language design to just a few attributes and a number of special rules. TBH that greatly reduced the number of things that can go wrong when doing the proof-of-concept.
  • adding trait and witness as top-level declarations may be OTT, given that all similar things like Struct and AbstractClass and Literal and so on have been added using attributes. But that can be tuned later.

Note that the prototype has some significant limitations, specifically that if explicit instantiation of the witnesses is allowed, then the "dictionary" being passed can't easily "close" over any values - as envisaged it must be passed by passing Unchecked.defaultof<Dictionary>. Passing actual non-null (and perhaps non-struct-typed) objects as dictionaries might solve this.

Copy link

Collaborator

cloudRoutine commented on Nov 3, 2016

It seems I misunderstood how this feature would work. I'd thought that a trait could only be used with witness and that a witness could only use a trait as its interface type.

If an interface with the [<Trait>] attribute can be used like a normal interface, would there be any reason not to adorn every interface with the attribute?

When an attribute needs to be used with a construct in all cases, isn't it effectively the same (from the programmer's perspective) as a top level declaration with extra boilerplate?

I suppose we'll have to rely on tooling to deal with the boilerplate disappointed

Copy link

Collaborator

dsyme commented on Nov 3, 2016

edited

If an interface with the [<Trait>] attribute can be used like a normal interface, would there be any reason not to adorn every interface with the attribute?

Off the top of my head there's no reason. We should look at the prototype though (which I felt was in-depth enough to determine questions like this)

Note that the prototype has some significant limitations, specifically that if explicit instantiation of the witnesses is allowed, then the "dictionary" being passed can't easily "close" over any values - as envisaged it must be passed by passing Unchecked.defaultof. Passing actual non-null (and perhaps non-struct-typed) objects as dictionaries might solve this.

I don't understand why this would be useful to allow - assuming you mean something like:

[<Witness>] // a.k.a instance
type EqInt(i:int) = 
    interface Eq<int> with 
        member equal a b = a = b
     member __.TheInt = i

Perhaps I'm missing something but allowing that looks very confusing to me.

I don't mind the attribute style at all. I'm all for keeping the number of keywords in F# as low as possible and building more and more one existing constructs in this manner, avoiding keyword salad.

I do rather like the jargon Rust uses for its type classes (trait and impl) though as I think it's more accessible to normal programmers, witness only makes intuitive sense to people in theorem proving circles, but I'm not super pushing for that to change here just noting my opinion.

One note: because these are written in terms of types it seems like they could never be extended to support statically resolved type parameters. Am I correct?

How would you support stuff like functor?

[<Trait>]
type Functor<'f> =
  abstract fmap: ???

Copy link

Collaborator

cloudRoutine commented on Nov 13, 2016

@Rickasaurus can you explain how the example I posted, or one similar to it, creates a "keyword salad"? I don't follow the point you're trying to make. The keyword is already reserved, so it's not like we can use it for ourselves.

because these are written in terms of types it seems like they could never be extended to support statically resolved type parameters. Am I correct?

I can't follow what you mean here either, can you give an example of what you'd like to do?

@Alxandr it can't be supported because we don't have type constructors

Copy link

Collaborator

dsyme commented on Nov 14, 2016

edited

@kurtschelfthout

Re explicit witnesses that close over values

I don't understand why this would be useful to allow

e.g. dependency injection (i.e. parameterization of a witness over some dependency):

[<Witness>] // a.k.a instance
type SomeWitness(someDependency: int->int) = 
    interface SomeTypeConstraint<int> with 
        member SomeOperation a b = someDependency a + someDependency b


... SomeConstrainedOperation(SomeWitness(myDependency),...) ...

let f () = 
    let myDependency x = x + 1
    ... some declaration that brings SomeWitness(myDependency) into scope ...

   ... SomeConstrainedOperation(...) ... // witness implicitly added here

The utility of this depends on the degree to which you use witnesses to propagate sets of functions which have a non-trivial dependency base. My understanding is that Scala implicits allow this technique. For example, witnesses propagated by implicits may capture a meta programming universe, which is a value.

I still think that not figuring out how to deal with type constructors will severely limit the usefulness of this proposal. Type classes without type constructors would allow for doing abstractions over numerical types, sure, but the lack of ability to do a generic bind and map (and similar) is in my experience what's hurting the most.

Copy link

Member

kurtschelfthout commented on Nov 15, 2016

edited

@dsyme I see, thanks for the explanation.

I don't have extensive experience with Scala implicits. While allowing bringing witness values into scope is more powerful, you also lose ability to reason about code. Here is the argument in more detail in case anyone is interested: https://www.youtube.com/watch?v=hIZxTQP1ifo

Disallow explicit instantiation of witnesses also means we can make them stateless structs and defaultof always works.

It does mean we would have some shenanigans like having to add wrapper types if one type can be a witness of a trait in more than one way (e.g. 'Monoid' and 'int', via either '+' or '*') but it looks to me like that is the vast minority of cases.

We then also have to think about coherence and orphan instances, e.g if there is more than one possible witness is in scope, warn or error, and have some explicit mechanism to disambiguate. even though this somewhat goes against the current F# practice of resolving names to the first thing in scope. Perhaps it would be enough to disallow orphans (i.e. defining a witness in a compilation unit without also declaring either the trait or the type), which would also cover pretty much all use cases I expect.

@Alxandr What you're asking for are higher-kinded types. The feature request for that is here. I don't think the two should be conflated.

Copy link

Collaborator

dsyme commented on Nov 15, 2016

@kurtschelfthout The video is good, thanks

While allowing bringing witness values into scope is more powerful, you also lose ability to reason about code.

I generally prefer arguments in utilitarian terms (bug reduction, safety under refactoring, stability of coding patterns under changing requirements, does a mechanism promote team-cooperation etc.). He makes some of these, though "reasoning about code" is not an end in itself, but can definitely give good utilitarian outcomes. But how many bugs (= lost developer time) are really caused by a lack of coherence, e.g. conflicting instances? I talked about this when last with Odersky and we figured it was very few. But how much time is spent fiddling around with type classes trying to get them to do the right thing, only later hitting some massive limitation like the inability to have local instances, or the inability to abstract instances with respect to a dependency?

A good example where lack of abstraction can hit is string culture. Let's say you create a huge amount of code that uses a swathe of string-related type classes that assume invariant culture (e.g. the ubiquitous show methods). Then you want to localize your code w.r.t. culture, and decide you want to do it properly (rather than via a thread local) and pass the culture as a captured parameter. But your type class instances can't be parameterized. So you either have to remove all those type classes from your code or resort to dynamic argument passing though thread-local state. Painful and discontinuous.

From what I see people in favor of type classes choose examples that are relatively context-free (no local instances required, or only in contrived examples), while people in favour of implicits choose examples where local instances are commonly needed (e.g. Scala meta programming examples, parameterizing by the meta-programming universe implied by a set of libraries). Both sets of examples also put on heavy pressure w.r.t. dependencies (e.g. types-with-nested-types as parameters - the scala meta-programming examples re replete with these) and higher-kinds.

Swift is another important comparison area since it is setting expectations in a new generation of devs.

Copy link

drvink commented on Nov 15, 2016

edited

@dsyme Scala implicits are plagued with problems, the least of which being that Scala's notion of parameterized modules is a runtime phenomenon, leading to issues like being able to have two instances of a Set parameterized by an ordering T and conflate them. This is claimed by some as an intentional benefit, but it seems categorically worse to have this "flexibility" than even the limitations of coherence that are imposed by a naive encoding of Haskell-style type classes, i.e. one lacking more complicated extensions like overlapping instances. The use cases for Scala-style implicits mostly seem to be either bandages for existing code or a form of syntactic sugar at best. Thread-local storage is indeed a hallmark of afterthought-oriented programming, but at least it's fairly explicit and gives no illusions of safety.

@kurtschelfthout Given that it's already possible to encode higher-kinded types to some degree via SRTP, this is probably the best time to have that discussion if we're already having the long-awaited one on type classes for F#, so I don't think @Alxandr is wrong to be bringing it up in this thread. It's difficult to imagine a type class mechanism incapable of Functor/Applicative/Monad bringing significant value; I don't think people want them in F# just so that they can write Show. (CEs are another good example of a feature that would be much more valuable if not for a limitation that feels too extreme; while the clumsiness of composing monads is not specific to F#, CEs and SRTP would at least be complementary features if CE implementation functions--Bind/Return/etc.--were allowed to be static members instead of only members.)

It's worth mentioning that the modular implicits1 proposal for OCaml solves many (all?) of the concerns related to both voiced so far in this thread. There is a shorter and more recent presentation3 from some of the designers as well for those curious.

1: arXiv:1512.01895 [cs.PL]
2: Modular implicits for OCaml - how to assert success

Copy link

Collaborator

dsyme commented on Nov 15, 2016

edited

Scala implicits are plagued with problems

Yeah, I know.

... leading to issues like being able to have two instances of a Set parameterized by an ordering T and conflate them. ...

Yes, I know. However TBH I don't think the case is proven this causes bugs in practice. When talking to Odersky about this recently I said that I doubted that a any production bugs had been caused by this problem, or at least vanishingly few. And the workarounds (such as using a unique key type for sets/maps, which is basically what we already do in F# if you need a custom comparer) are not particularly problematic. Certainly the situation is no worse than the existing .NET collections.

Anyway I'd need to see much stronger utilitarian evidence that this truly is as critical as claimed - it seems like a well-intentioned article of mathematical faith (and one which I would wholeheartedly subscribe to from a purely academic perspective) more than one grounded in the reality of software practice. To contrast, the problems of "not being able to parameterize or localize my instances" are very much grounded in modern software reality and standard F# practice. In F#, being able to parameterize a thing by values is very fundamental, even if you have to plumb a parameter to each function of a module or member of a class explicitly. Active patterns, for example, can be parameterized, for good reasons.

The use cases for Scala-style implicits mostly seem to be either bandages for existing code or a form of syntactic sugar at best.

From the F# perspective the whole thing is really syntactic sugar just to pass some parameters implicitly :)

I would like to see an analysis of the extra powers of Scala implicits by someone who favors the mechanism and uses it well, or at least can speak to its use cases. Some of the use cases I've seen in the area of meta-programming look quite reasonable. The mechanism has problems though.

I'll look at the modular implicits work again, it's been a while. Last time I looked it would need extensive modification to be suitable for F#, and it didn't strike me that F# needed to aim for the same goals, but I'll look more carefully. It's a very tricky area to be honest, so many subtleties.

I rather like that Scala will give you an error with an ambiguous instance. Ideally it wouldn't matter, but F# is neither pure nor lazy and so it seems much safer to me to be sure about which instance you're using.

Along these lines I think tooling for this feature might be extremely important. It will certainly be necessary to have an easy way to figure out which instance is being used and where it lives.

@drvink
Side note: I remember suggesting parameterized modules a long time ago, although I wasn't clever enough to see the relationship with type classes back then. What I wanted them for was mostly being able to avoid using classes in cases where some static parameterization was required up front. Also figured it might be used to make certain code more efficient, if the compiler was smart about it.

Modular implicits are pretty neat. I like that they are very explicit with their syntax and so it's more clear to beginners what is going on. One of the weakness (but also paradoxically a great strength) of Haskell is that there is so much magic going on that it ends up taking a lot of mental gymnastics to understand what complex code is doing because so much is inferred. Although, that magic also leads to very terse code.

@dsyme

A good example where lack of abstraction can hit is string culture. Let's say you create a huge amount of code that uses a swathe of string-related type classes that assume invariant culture (e.g. the ubiquitous show methods). Then you want to localize your code w.r.t. culture, and decide you want to do it properly (rather than via a thread local) and pass the culture as a captured parameter. But your type class instances can't be parameterized. So you either have to remove all those type classes from your code or resort to dynamic argument passing though thread-local state. Painful and discontinuous.

You put the many possibilities we already have to propagate values implicitly (statics, thread locals, whatever the thing is called that propagates across async calls) in a negative light, perhaps rightly so. What is the advantage of adding another implicit value propagation mechanism - how is it that much better than the existing ways?

Concretely, in the string culture example. Without implicit value passing, we can change all the witnesses to take CultureInfo.CurrentCulture into account instead of the invariant culture, or refer to some other global. Then we have to make sure that the right value for that is set at the right places in the program. Where this sort of thing needs to be scoped statically I've usually resorted to using blocks in the past, and that seems to work out pretty well.

With implicit value passing, we very similarly have to change all the witnesses to take an extra constructor argument - the culture - and use it in the implementation. And then we have to make sure that the right implicit value is brought in scope at the right places in the program. Perhaps I am missing something but it feels very similar.

On the positive side, my main reasons for supporting this proposal is to:

  1. Support open extensibility - i.e. allow existing types (that I don't control the code for or don't even know exist yet) to be treated as if they implement an interface (trait).
  2. Support what I will loosely call abstraction over constructors - i.e. allow traits with methods like Create : 'T

Don't know if it's me but I keep running into this limitation, and there are no clean workarounds (I know, because I've worked around them many times in different ways). One example is FsCheck allows you to check any type that it can convert to a Property, which is unit, bool, and functions that return any of those (among other things). But the type of check can only be : 'P -> unit note no constraint or indication whatsoever on what this 'P can be, no way for the user to extend allowable types, and consequently hard to document what is actually going on here, leading to much confusion. Something like: Property 'P => 'P -> unit would be so much nicer, esp. if the tooling would catch up and you'd be able to look up straightforwardly what all the witnesses are for Property that are in the current scope. In my estimation, this would significantly reduce the learning curve for new users, improve the documentation, and give advanced users an extra useful (and easily discoverable) extension point.

I realize you can do all of that with implicit values too, because they're strictly more powerful, but I just feel I already have plenty of choice to access values implicitly - perhaps even too many :)

I've used implicits in scala (that being said I've used scala for all of about 2 weeks, so I'm no expert). And what it was used for was passing an execution context around to async code. Basically, it served the purpose of System.Threading.Tasks.TaskScheduler.Current. That being said, implicits might be a better way to handle this than static getters (backed by threadstatic values and other black magic), but I still think that it should be taken into consideration that .NET already has a idiomatic (I think I'm using this word correctly) way of dealing with ambient contexts. And if that needs to be changed I think that's something that should probably be agreed upon by the entirety of the .NET community. I also think these are two different issues. Type classes deals with abstractions of features, whereas implicits are way to implicitly "attach" context to functions. Not to mention the fact that they aren't even mutually exclusive since scala has both (sort of).

Also, I agree with @drvink that while allowing people to implement Show is cool and all, it might also cripple traits into being a niche feature that nobody uses without also figuring out how to deal with type constructors at the same time, with or without CLR support.

Copy link

Savelenko commented on Nov 18, 2016

edited

@Alxandr As a practicing "enterprise" software engineer, I can assure you, that traits are much needed today, while most of engineers in my immediate environment, which I consider typical, cannot and need not grasp the concept of type constructors in order to be more productive and output better architected programs due to traits. It's just that day-to-day programming does not involve writing (library) code which abstracts over type constructors. But also conceptually, it is not the case that traits as discussed here are "severely limited", because traits/type classes are about polymorphism/overloading, while type constructors are about which terms are considered legal types in a programming language. The two notions are quite orthogonal and we should not mix them here.

Fwiw, my 2c: I've been playing around recently with a very simple dictionary-passing approach to typeclasses (encode the instances as record values holding the operations as functions), see e.g. https://github.com/yawaramin/fsharp-typed-json/blob/ae4c808d3619e3703451211ba2bf079cb6c61bc0/test/TypedJson.Core.Test/to_json_test.fs

The core operation is a function to_json : 'a To_json.t -> 'a -> string which takes a typeclass instance and a value, and converts the value to a JSON string using the typeclass instance. This is fairly simple and easy to implement and use, but the thing that keeps it short of being 'magical' is that I have to manually pass in the instance. Here's the relevant part of the definitions:

module To_json =
  type 'a t = { apply : 'a -> string }
  ...
  module Ops = let to_json t = t.apply

Now, if I could instead mark parameters as implicit, say e.g. with #: let to_json (#t : 'a t) = #t.apply and we had a syntax rule that implicit parameters must always be declared first, perhaps.

And correspondingly declare the instances as: let #string : string t = { apply = sprintf "\"%s\"" }

The compiler would have to convert calls like to_json "a" into to_json #string "a", after finding the implicit with some implicit search mechanism. And that makes it 'magical' again.

Copy link

Member

kurtschelfthout commented on Nov 24, 2016

edited

Swift is another important comparison area since it is setting expectations in a new generation of devs.

Indeed. I think the closest Swift comes to something like this is through protocols. Compared to interfaces, besides methods and properties they can impose static methods and constructors on the implementing entity. Also some requirements can be specified as optional (you then need to use optional chaining, like the ?. operator in C# to call these. Not really relevant to this discussion). Finally protocols can provide default implementations. So really they are a sort of halfway between interfaces and abstract classes. More possibilities than interfaces, less than abstract classes (in particular they can't define fields), but this allows more flexibility down the line (e.g. a type can be a subtype of multiple traits).

Swift then allows implementing these on types much in the same way as interfaces/abstract classes, but it also allows "protocol extensions". Again comparing to .NET these are like extension methods, but for entire protocols. In this sense, protocol extensions are close to what was proposed in #182.

It's interesting also that like extension methods, protocol extension can impose additional requirements on the extended type at the point of extension using type argument constraints. The example they give is, translated to fictional F# syntax:

//ICollection<'TElement> an existing type
//this extends all ICollections to be also TextRepresentable (another interface/protocol)
//_if_ their elements are also TextRepresentable.
type ICollection<'TElement when 'TElement:TextRepresentable> with
    interface TextRepresentable with
        member self.TextualDescription =
            let itemsAsText = self |> Seq.map (fun elem -> elem.TextualDescription)
            "[" + string.Join(", ", itemsAsText) + "]"

This is very close to how protocols in Clojure work - except they are not typed.

It seems to me that this is qualitatively different from type classes or implicits. In particular, type classes are a static overloading mechanism. Implicits are syntactic sugar to have the compiler pass implicit arguments to functions. UPDATE Protocols allow you to extend dynamic dispatch (the vtable, in some sense) on existing types after the fact. This is wrong, the methods on protocol extensions are statically dispatched, see here and here. I don't know enough about modular implicits in OCaml to comment how it related in one sentence.

In terms of votes this wide range of possibilities for this one suggestion seems problematic, but then of course we have a BDFL @dsyme so the votes are just to appease us unwashed masses anyway ;)

Perhaps it makes more sense to have a goal-directed discussion, instead of focusing on mechanisms. What can't you express right now (or is awkward to express) that you think this suggestion should address? (I gave my 2c on that in an earlier comment)

Copy link

Collaborator

dsyme commented on Nov 25, 2016

Perhaps it makes more sense to have a goal-directed discussion,

@kurtschelfthout I'd like to see someone trawl through the various uses of protocols in Swift and pick out 3-4 examples (which couldn't be achieved by OO interfaces, and which feel different in application to type classes)

@dsyme There are a number of use cases of protocols and protocol extensions in the video and slides here: https://developer.apple.com/videos/play/wwdc2015/408/

(note also my update in the comment above - protocol extensions are static constructs, closer to typeclasses than I originally thought, but with more of an OO "feel".).

I had a talk with gusty about this topic and we agreed on to first work on a fully fleshed spec so the problems we have currently with SRTP's don't happen again. Then we can talk about feature branches with easy access or CI builds for people who want to try it out.

If we manage to get to this point, we'll be in a position where we would be practically one to one with "Typeclasses with no HKTs", see above in the thread my points about no HKTs.

emphasis mine

I can't find this :/ @gusty

@narfanar this one -> #243 (comment)

Just to be sure:

Higher-order modules are still off the table, right? Since I think they could provide the most elegant solution to this... Thanks :)

Copy link

Contributor

Happypig375 commented on Aug 25, 2019

Pretty sure that counts as "already been decided in previous versions of F#".

of the table

Does that phrase mean "declined" or "still considered as a possible feature"?

Copy link

Member

cartermp commented on Aug 26, 2019

See here: #351

I am working in a quant firm, only reason we dropped F# was because this feature is missing. In the end we settled with haskell

Copy link

Swoorup commented on Jan 30, 2020

edited

A good use case/example would be similar to the way servant-swagger integration works, i.e adding servant (Haskell REST library) swagger style documentation in an ad-hoc fashion. For example, in servant I can define my types which are user facing and get it translated to JSON in a Web Api project.

I can then add documentation as a separate project, implementing ToSchema for the types I am exposing. GHC will complain if I miss providing a ToSchema instance for a particular type, since it has knowledge about all types exposed. This is very useful if you are working on a large project.

Any update on this?

Copy link

Member

7sharp9 commented on Jun 3, 2020

I thought the status was, not until C# has them.

I would like to add a real-world use case for where I believe this would be helpful. I have been working on a library which is meant to represent a specific mathematical domain: Linear Programming. My ideal scenario is that I can define a simple algebra for combining these types without having to use excessive amounts of operator overloading. As of now, I simply brute forced it with just creating overloads for every permutation. Here is what I would like to have done and what I believe type classes/implicits would help with.

Currently the domain is made up of four types: float, Scalar, Decision, and LinearExpression. A Scalar wraps a float and defines strict rules around equality. I wanted to support working with float as well though for easy "ergonomics" when using the library. Here are the legal operations:

// The `+` operations
float + float -> float
float + Scalar -> Scalar
Scalar + Scalar -> Scalar
float + Decision -> LinearExpression
Scalar + Decision -> LinearExpression
Decision + Decision -> LinearExpression
float + LinearExpression -> LinearExpression
Scalar + LinearExpression -> LinearExpression
Decision + LinearExpression -> LinearExpression
LinearExpression + LinearExpression -> LinearExpression

// The `*` operations
float * float -> float
float * Scalar -> Scalar
Scalar * Scalar -> Scalar
float * Decision -> LinearExpression
Scalar * Decision -> LinearExpression
// Not Allowed: Decision * Decision -> compiler error
float * LinearExpression -> LinearExpression
Scalar * LinearExpression -> LinearExpression
// Not Allowed: Decision * LinearExpression -> compiler error
// Not Allowed: LinearExpression * LinearExpression -> compiler error

Note, all of these are commutative so I had to implement them with the ordering reversed as well. This is a brute force approach. Now, I believe if you have something like type classes or implicits you could do something like the following (Please forgive my ignorance if I am off base though). I made up a notation that I think captures the idea. I wrote out all of the types for clarity

// Define typeclass for Scalar addition 
typeclass ScalarTypeClass<'a> =
    abstract member AsScalar: 'a -> Scalar
    default member (+) (x:ScalarTypeClass, y:ScalarTypeClass) : Scalar =
        let xScalar = x.AsScalar
        let yScalar = y.AsScalar
        // Scalar addition defined here

    default member (*) (x:ScalarTypeClass, y:ScalarTypeClass) : Scalar =
        let xScalar = x.AsScalar
        let yScalar = y.AsScalar
        // Scalar multiplication defined here

// Define typeclass for LinearExpression addition
typeclass LinearExpressionTypeClass<'a> =
    abstract member AsLinearExpression: 'a -> LinearExpression
    default member (+) (x:LinearExpressionTypeClass, y:LinearExpressionTypeClass) : LinearExpression =
        let xExpr = x.AsLinearExpression
        let yExpr = y.AsLinearExpression
        // LinearExpression addition defined here

    default member (+) (x:LinearExpressionTypeClass, y:ScalarTypeClass) : LinearExpression =
        let xExpr = x.AsLinearExpression
        let yScalar = y.AsLinearExpression
        // LinearExpression addition defined here

    default member (+) (x:ScalarTypeClass, y:LinearExpressionTypeClass) : LinearExpression =
        let xScalar = x.AsScalar
        let yExpr = y.AsLinearExpression
        // LinearExpression addition defined here

    default member (*) (x:LinearExpressionTypeClass, y:ScalarTypeClass) : LinearExpression =
        let xExpr = x.AsLinearExpression
        let yScalar = y.AsLinearExpression
        // LinearExpression multiplication defined here

    default member (*) (x:ScalarTypeClass, y:LinearExpressionTypeClass) : LinearExpression =
        let xScalar = x.AsScalar
        let yExpr = y.AsLinearExpression
        // LinearExpression multiplication defined here

Now that I can define these interactions in a single place, I can make sure that the appropriate types have AsScalar and AsLinearExpression. In my case, float and Scalar must provide an AsScalar static method. This means I need to be able to extend the existing float type. Something that cannot be done at this time. I also provide the AsLinearExpression static method for the Decision and LinearExpression types.

Defining these operators in a single place rather many times over would drastically reduce the complexity of my library and make it easier to maintain. I have not even mentioned all of the operator overloading required to support the ConstraintComparison operators: ==, <==, and >==. Eventually I would like to consider adding more types and interactions to this domain but the operator overloading management would become unwieldy. I have examined other approaches but the need for a highly usable library with simple "ergonomics" was prioritized over reducing the size of the library.

If Type Classes / implicits are not the abstraction I am looking for, I would love to hear it! I have been searching for a solution to operator overload explosion. Taming operator overload explosion would be a huge deal for the domains I work in.

So when can we expect C# to catch up ?

So when can we expect C# to catch up ?

When my grandson dies of old age zany_face

This is the most thumbed-up suggestion in fslang-suggestions and is over 7 years old. Is there any hope this will ever happen?

Copy link

vzarytovskii commented 10 hours ago

edited by dsyme

This is the most thumbed-up suggestion in fslang-suggestions and is over 7 years old. Is there any hope this will ever happen?

We don't have any updates on it at the moment .

Copy link

Collaborator

dsyme commented 6 hours ago

edited

This is the most thumbed-up suggestion in fslang-suggestions and is over 7 years old. Is there any hope this will ever happen?

My position is pretty clear. I'll recap it here.

  1. The utility of type classes for the kind of "functions + data" coding we aim to support in F#, in the context of interoperating with .NET and Javascript, is largely over-rated and has many negative aspects that are rarely considered in a balanced way by those advocating these kinds of features. Some examples of the negative consequences are below:

    • Simple type-classes are never sufficient and result in a consistent stream of requests for more and more type-level programming and type-level abstraction (higher kinds, higher sorted kinds, type families, whatever).
    • Any advanced combination of type-class-like features can very often result in obfuscated, subtle code
    • Adding features in this space leads to a trajectory towards more and more type-level programming. This has serious compilation performance downsides.
    • Adding features in this space leads to a need for compile-time debugging. This is absolutely real - the C++ compiler gives "stack traces" of template instantiation failures, for example. Yet this is a kind of debugging that's completely unsupported in any IDEs today.
    • Adding hierarchical type classification can result in programming communities that spend most their time talking about the "right" way to organise the universe of type classes, and experience surprisingly dogmatic discussions about that
    • Adding hierarchical type classification can result in programming libraries exposed to the "fragile type classification" problem - and repeatedly "rejig" and "tweak" their basic type classifications. This is not possible in a binary compatible ecosystem, meaning we'd be unlikely to ship any hierarchy of classifications in FSharp.Core.
    • Adding type-level programming of any kind can lead to communities where the most empowered programmers are those with deep expertise in certain kinds of highly abstract mathematics (e.g. category theory). Programmers uninterested in this kind of thing are disempowered. I don't want F# to be the kind of language where the most empowered person in the room is the category theorist.
    • The most effective use of these features require end-to-end design of the entire API surface area with these features in mind. Everything becomes tied up into a particular view on type-level abstractions and type-level programming. It quickly becomes impossible to program without an understanding of these abstractions, or without referencing a library which encodes this

    All in all this combination can actually be worse than deeply nested object-oriented hierarchies, for example, which F# also strongly discourages.

  2. We will eventually progress this approved RFC https://github.com/fsharp/fslang-design/blob/main/RFCs/FS-1043-extension-members-for-operators-and-srtp-constraints.md. A pre-requisite to this was https://github.com/fsharp/fslang-design/blob/main/FSharp-5.0/FS-1071-witness-passing-quotations.md and, while that's in F# 5.0, we've still got one outstanding issue related to that. However I am aware that it is an opportunity to resolve all outstanding issues regarding SRTP. I also won't progress it until I am certain that it won't lead to a considerable rise in attempts to use type-level programming in F# for activities outside those designed to be supported by the RFC.

  3. C# has already added features relevant to this space, e.g. static abstract methods. We must consider the ramifications of these for F# and integrate these.

  4. We will not progress a type class design independently of C# (beyond SRTP, FS-1043, static abstract methods and so in)

  5. TypeScript and typed Python are incorporating type-level programming programming features that are practically based, for interop/API-typing purposes. These features have many of the problems described above, but are at least using a methodology far more relevant to F# than the "hey, let's make programming maximally abstract, maximally inferred and as much like category theory as possible" agenda sometimes pursued. That is, if we add more type-level programming features they would be much more likely to be the sort of thing to support the interop needs of Fable or F#-interop-to-Python or similar.

As an aside, something strange happens when one tries to have rational conversations about the above downsides with people strongly advocating expansion of type-level programming capabilities - it's almost like they don't believe the downsides are real (for example they might argue the downsides are "the choice of the programmer" or "something we should solve" - no, they are intrinsic). It's like they are prioritising type-level programming over actual programming. Indeed I believe psychologically this is what is happening - people believe that programming in the pure and beautiful world of types is more important than programming in the actual world of data and information. In contrast, the downsides are real and can have a thoroughly corrosive effect on the simple use of a language in practice in teams.

As one example, I've translated Swift code using protocols to F#, and the use of Swift protocols was needless, and the resulting F# was much, much simpler (and much more like the original Python). It was very obvious to me that the person translating these samples used Swift protocols to somehow try to show off what so-called "value" Swift was bringing to the table. Well they failed - the use of protocols was simply obfuscating, not helpful. This happens all the time once extensive type-level programming facilities are available in a language - they immediately, routinely get mis-applied in ways that makes code harder to understand, excludes beginners and fails to convince those from the outside.

Copy link

Contributor

Happypig375 commented 6 hours ago

edited

What happened? Why delete? Why edit a large comment to "."?

(Edit for context: 2 deletions happened before the previous comment, and the one of the edit histories of the previous comment was just a .)

Copy link

Collaborator

dsyme commented 6 hours ago

edited

What happened? Why delete? Why edit a large comment to "."?

My apologies :)) I got in a huff and deleted two messages that were perfectly reasonable - I don't even know why I did it - there was absolutely nothing wrong with the messages, the whole thread was just getting a bit off track. Anyway the messages were totally fine and I will find the original text of the messages and repost them below.

Comment #1 By robertj

Why catch up? If it all C# will have them first (if at all) and only then F# will implement such a feature

Comment #2 By SchlenkR

IMHO the way F# progresses seems to be „fear-driven“ since a while. The fear is that scenarios like async/task, quotations/linq, tuples, etc. will occur again when F# is evolving in major steps, so it’s evolving in minor steps only. Although I personally would like to see F# progression going another way, I can understand the decisions being made and they are legitim, even if I don’t share them. There are still heavily weighting argument to choose F# in favour of C#, and these reasons are fundamental to me (HM type inference, expression based, some more), and C# will never „catch up“ with those I guess. And if I had the choice of having TC/HKT, and „F# having the best IDE support of all languages“, it would be a really hard decision. What I want to say is: there are many „minor“ things that can be done to improve the user experience of F# on a major level, and I would be happy to see progression in those fields, too. And one day... I will find a customer using Haskell only blush

Again, my apologies

Copy link

Collaborator

dsyme commented 6 hours ago

I have added a link to my response within the text of the main suggestion.

Copy link

JustinWick commented 5 hours ago

edited

What happened? Why delete? Why edit a large comment to "."?

My apologies :)) I got in a huff and deleted two messages that were perfectly reasonable - I don't even know why I did it - there was absolutely nothing wrong with the messages, the whole thread was just getting a bit off track. Anyway the messages were totally fine and I will find the original text of the messages and repost them below.

It's my hope some day to write something truly great here that's just edgy enough to get deleted, but reasonable enough that that person feels bad about deleting it.

That's the F# I want to see in the world--something beautiful that's just a little too much.

(Note: this comment is one of those defuse-a-situation jokes and should not be taken seriously)

...but reasonable enough that that person feels bad about it

I don't want to make anyone feel bad - no matter if it's the content or the form - so my apologies if I triggered something like that; that was definitely not my intention.

Thans for the enlightening comments, @dsyme - many of your arguments weren't even in my mind, or at least not so clearly, and I find it good to emphasise that there are a lot of good arguments independent from "not until C# has it".

A quote from Antoine de Saint-Exupery: "Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away." - that's why I love F#.

Copy link

matthewcrews commented 3 hours ago

edited

As someone who believes they would greatly benefit from Type Classes, I would like to offer a thought. There is no idea that I have NOT been able to express in F# as it exists today. Now, I may have to write more method overloads than I would like, but it is expressible. The lack of Type Classes has pushed my design in different directions than I wanted at first but I was still able to achieve the desired outcome.

Copy link

drvink commented 3 hours ago

edited

Since this topic is so frequently revisited, it's worth mentioning that Haskell-style typeclasses can be fully encoded in F# today, and those who wish to use some of the the "famous" abstractions (Functor, Applicative, Monad, etc.) can have them off-the-shelf via F#+; furthermore, for those interested in the actual encoding via F#'s constraint mechanisms, @cannorin has written a lovely article in Japanese which happens to machine translate quite well for anyone who wants to understand the ideas and/or extend them for their own purposes.

I can't help but mention as a side note (as an also-Haskell user!) that the experience of writing out explicit constraint invocations in F# (and burying them in implementation details/a library) and with F#+ has been less painful than extensive use of type classes in GHC/Haskell itself. Make of that what you will (though I did pay my dues many years ago for both.)

"C# has already added features relevant to this space, e.g. static abstract methods. We must consider the ramifications of these for F# and integrate these."

Is there an issue for tracking this yet? It's already a pleasure to use in C# preview.

(Note that this feature, on its own, allows definitions of typeclasses of kind * like Semigroup/Monoid.)

"C# has already added features relevant to this space, e.g. static abstract methods. We must consider the ramifications of these for F# and integrate these."

Is there an issue for tracking this yet? It's already a pleasure to use in C# preview.

I haven't seen one yet. If you can create feature request to track it, it would be great!

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

No one assigned

Projects

None yet

Milestone

No milestone

Linked pull requests

Successfully merging a pull request may close this issue.

None yet

44 participants
and others

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK