Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Arithmetic instances in LangaugeExt.TypeClass #1024

Closed
benjstephenson opened this issue Apr 24, 2022 · 4 comments
Closed

Arithmetic instances in LangaugeExt.TypeClass #1024

benjstephenson opened this issue Apr 24, 2022 · 4 comments

Comments

@benjstephenson
Copy link
Contributor

I'm pretty new to the library (really loving it 👌) and C# in general but I've come from a Scala Cats etc world so I'm not sure if I'm just trying to overlay what I've learned in that space too much and getting myself confused a bit.

I can see that Arithmetic defines the Plus, Subtract, Negate and Product capabilities that Num extends from, I've got a custom data type that makes sense to have these implemented but not the rest of the methods that Num defines.
I've tried to use the subtract etc static methods from Prelude but found that they expect a null Num instance:

public static A subtract<NUM, A>(A x, A y) where NUM : struct, Num<A> => default (NUM).Subtract(x, y);

Should these methods accept an instance of Arithmetic as I think that's the "least powerful" typeclass that could be used?

@louthy
Copy link
Owner

louthy commented Apr 24, 2022

but found that they expect a null Num instance

This sentence ^ confused me a bit, because instances shouldn't ever be null. So, I'll walk through how it works in C# in case there's a gap in your understanding. Apologies if I'm explaining what you already know....

C# doesn't have 'official' support for ad-hoc polymorphism (other than the static interface methods in the latest C#, but that hasn't any backward compatibility). However, there's a way to implement it by leveraging the fact that struct cannot be null (combined with constraints).

So, for example - this is a 'trait' or 'type-class' defined as an interface (sticking to monoid, as it's very simple):

    public interface Monoid<A>
    {
        A Empty();
        A Append(A x, A y);
    }

Then the 'instance' can be implemented as a struct:

    public struct MSeq<A> : Monoid<Seq<A>>
    {
        public Seq<A> Empty() => Seq<A>();
        public Seq<A> Append(Seq<A> x, Seq<A> y) => x + y;
    }

If I do this:

    var mx = default(MSeq<int>);

Then mx won't be null (because MSeq is a struct). And so I can then call the methods on it:

    var xs = mx.Empty();
    var ys = mx.Append(xs, Seq(1, 2, 3));

We don't ever pass instances around as data, we just need to provide them as generic arguments and constrain them. Here's a general example of Append (mappend in Haskell)

    public static A Append<MA, A>(A xs, A ys) where MA : struct, Monoid<A> => 
        default(MA).Append(xs, ys);

NOTE: The where constrains MA to be a struct and Monoid<A>. That means default(MA) will generate a non-null instance, which we can then invoke methods on (as shown before).

but found that they expect a null Num instance

Going back to this ^. I'm not sure what I wrote has clarified the fact that instances can't be null (when constrained properly). So, if you're still stuck then I might need some more concrete examples

Should these methods accept an instance of Arithmetic as I think that's the "least powerful" typeclass that could be used?

No, you must provide an instance that satisfies the maximum, or the "most powerful". I haven't worked with Scala, but that does sound a bit odd to me (accepting the least powerful). I'd always expect a constraint to want to be completely satisfied. I'm sure Haskell works that way (when explicitly stating the constraint, rather than letting the type-inference do it for you - in which case it would be the minimum).

There's a write-up on the README about ad-hoc polymorphism in C#. Honestly, you might want to leave some of those generalist ideas in Scala-land, as it can get pretty ugly in C# (due to the lack of generic inference), and is definitely not idiomatic. However, it is type-safe, and it is legitimate ad-hoc polymorphism. Definitely forget the idea of trying to implement higher-kinds with it though, that's an ugly process.

@benjstephenson
Copy link
Contributor Author

Agh, sorry that 'null' was a typo. That explanation is helpful though - does the default(MSeq<int>) call end up calling a default constructor for the struct?

I guess my confusion is that the Arithmetic interface is the one that defines the ability to add or subtract and it doesn't extend from Num so I interpret that as "the ability to 'add' or 'subtract' depends on being an instance on Arithmetic and not Num" which is why I expected the static instance of subtract to be defined against Arithmetic like so

public static A subtract<ARITH, A>(A x, A y) where ARITH : struct, Arithmetic<A> => default (ARITH).Subtract(x, y);

Similarly, the Append capability is defined on Semigroup, so the static instance append takes a Semigroup instance, and not something more specific like Monoid (looking here).

I'm really still trying to find my way with the C# type system though so you're right there's probably a load of stuff I need to leave out of my thinking; I'll give the readme another look, thanks heaps :)

@louthy
Copy link
Owner

louthy commented Apr 24, 2022

does the default(MSeq) call end up calling a default constructor for the struct?

No, structs (value types) are special in C#, they don’t have default ctors, they just zero the memory needed, which brings perf benefits for things like arrays of structs.

In this case the compiler optimises away the struct allocation and just calls the methods direct (because there’s no fields, and are therefore zero size)

You’re right that I should have used the more precise trait for subtract, and probably others. This is probably an artefact of me adding Num first and the other traits later. Feel free to open a PR if you’d fancy changing them.

@benjstephenson
Copy link
Contributor Author

Ah I see, that makes sense.
Cool, I'll give it a go and submit one this week. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants