Ok, but someone has to manually check that, since someone could write "default(MA)" with MA not being a struct and this wouldn't be obvious at the call site. And even if we do find a way to automatically enforce that it is a struct, default won't necessarily put it in a valid state, right? (e.g. if the struct contains reference types then we've just moved the problem one step down: the struct can't be null but the things inside the struct can be null).
Edit: Also does this "default" mechanism extend to allowing us to compose typeclass instances out of smaller typeclass instances? E.g. the monad instance for Writer is defined as:
instance (Monoid w) => Monad (Writer w) where
return a = Writer (a,mempty)
(Writer (a,w)) >>= f = let (a',w') = runWriter $ f a in Writer (a',w `mappend` w')
i.e. we can obtain a Monad<Writer<W, ?>> for any W for which we have a Monoid<W>.
Well if you don't care about type safety then there's no point caring about any typesystem features, since you can emulate them by replacing all of your types with "any".
Sorry, where on earth did I say I don’t care about type safety? Why do you need to take this point to a total extreme? I simply gave an example of why the comment about mempty was wrong; but now I have to defend C#’s type system?
Clearly C#’s lack of type inference, sanctioned ad-hoc polymorphism (even though it can’t be achieved in the way I have shown), and higher kinds makes it less expressive as a language. I’m not going to argue that point.
But this kind of language holy war is frankly pathetic. Attacking every detail of an implementation (that works) is unnecessary.
Yes, it’s easier to get null reference exceptions in C# compared to Haskell. That is the result of poor decisions made when the language was designed. So, yes, today I will use ad hoc polymorphic techniques and yes I will have to make sure I constrain to structs, that’s life.
> Sorry, where on earth did I say I don’t care about type safety? Why do you need to take this point to a total extreme? I simply gave an example of why the comment about mempty was wrong; but now I have to defend C#’s type system?
If you're going to dismiss safety issues in your approach with "Yes, it’s possible for programmers to write bugs." then there's no point having the conversation, because that's an equally good argument for not having a type system at all.
> But this kind of language holy war is frankly pathetic. Attacking every detail of an implementation (that works) is unnecessary.
It's not a "detail", if you can't do it safely then that undermines the point of doing it at all. If we were willing to be unsafe we could just cast to the desired type.
> Yes, it’s easier to get null reference exceptions in C# compared to Haskell. That is the result of poor decisions made when the language was designed. So, yes, today I will use ad hoc polymorphic techniques and yes I will have to make sure I constrain to structs, that’s life.
I'd sooner pass the module dictionary explicitly, like one does in ML, than adopt a technique that would normalize having "default(...)" in my codebase.
> If you're going to dismiss safety issues in your approach with "Yes, it’s possible for programmers to write bugs." then there's no point having the conversation, because that's an equally good argument for not having a type system at all.
Absolute nonsense. I didn't dismiss safety issues at all. I dismissed your claim that having to specify a `struct` constraint somehow makes the feature unworthy.
C# has null, that's a fact of life, it's not dismissive to realise that a (granted, very annoying) part of the job of writing C# is dealing with null. So, using this doesn't make this technique any less safe than any other way of writing code in C#. So, yes, programmers will occasionally write null-dereference bugs in C# - that's the price we pay for bad language implementation decisions.
Stating "that's an equally good argument for not having a type system at all." is clearly hyperbolic nonsense.
> If we were willing to be unsafe we could just cast to the desired type.
But it isn't unsafe! Not specifying a `struct` constraint is a bug. If you provide the constraint then it's safe. Trying to compare that to a dynamic cast where you have no type-system enforcement to one where you do is just idiotic.
> I'd sooner pass the module dictionary explicitly, like one does in ML, than adopt a technique that would normalize having "default(...)" in my codebase.
At no point was this trying to force you to use this technique. It was a reply to "Once you've got return type polymorphism, you really start to miss it in other languages. The simplest example possible is mempty".
I use this technique very successfully a lot, and the exact mechanism (of using `default`) is in the process of being wrapped up into a new type-classes grammar for C# [1]. So, I guess you'd probably prefer to wait for that...
> C# has null, that's a fact of life, it's not dismissive to realise that a (granted, very annoying) part of the job of writing C# is dealing with null. So, using this doesn't make this technique any less safe than any other way of writing code in C#.
If using this technique requires breaking one of the rules that you have to follow to avoid getting nulls in C# then the technique is a safety problem.
> Not specifying a `struct` constraint is a bug. If you provide the constraint then it's safe.
Ok, but how do you enforce that? If you've got a technique that requires manual review and reasoning at a distance to use safely, then again we're no better off than we would be using dynamic casts.
> At no point was this trying to force you to use this technique. It was a reply to "Once you've got return type polymorphism, you really start to miss it in other languages. The simplest example possible is mempty".
If you don't have a typesystem feature in a safe way, you don't have it.
> Ok, but how do you enforce that? If you've got a technique that requires manual review and reasoning at a distance to use safely, then again we're no better off than we would be using dynamic casts.
More hyperbole. Failing to constrain may lead to a null reference exception. Just like passing a reference to any method anywhere in C#. It is no better and no worse than any other C# code. However it does allow for ad-hoc polymorphic return values. Which is the entire point. That is not the same as returning a dynamic value, which is a type that propagates dynamic dispatch wherever it's passed. A failure to capture a null reference bug means on first usage it will blow up - so you fix the code and everything is type safe.
> If you don't have a typesystem feature in a safe way, you don't have it.
The feature is safe. Your argument is the same as saying C# doesn't have classes because a reference can be null, or C# doesn't have fields because a field can be null. All throughout this frankly tedious discussion you have somehow conflated having a bug in an application with having no type system at all. C#'s type system is obviously nowhere near as impressive as Haskell, but C# is actually used in the real world much more, and so if someone wants polymorphic return values then they can. I mean they can anyway through inheritance, never mind the ad-hoc approach I demonstrated - but whatever yeah?
> Failing to constrain may lead to a null reference exception. Just like passing a reference to any method anywhere in C#.
But you can adopt a small set of rules that are locally enforceable (and practical to use in an automatic linter) to prevent this happening (just as Haskell is safe even though unsafePerformIO exists, because you can adopt a small set of locally enforceable rules like "never use unsafePerformIO"). Unfortunately one of those rules has to be to never use default().
> That is not the same as returning a dynamic value, which is a type that propagates dynamic dispatch wherever it's passed. A failure to capture a null reference bug means on first usage it will blow up - so you fix the code and everything is type safe.
Unfortunately default() isn't fail-fast in all cases - when used with e.g. a struct type containing a reference type, it will create the value in an invalid state (containing a null reference) but you won't necessarily notice until you come to use the value, arbitrarily many compilation units away. So it's just as dangerous as a dynamic value.
> All throughout this frankly tedious discussion you have somehow conflated having a bug in an application with having no type system at all.
In almost any language you can have polymorphic return values without complete type safety. The feature that Haskell has here isn't that you can have polymorphic return values - it's that you can have polymorphic return values safely. Showing an unsafe implementation of polymorphic return values in some other language is pointless and irrelevant.
> Unfortunately default() isn't fail-fast in all cases
It's purely a means of dispatch, if someone wants to put member variables in that are never used - good luck to them. For some reason you think that because C# doesn't protect you from being an idiot you can't do return type polymorphism. Well that's completely incorrect and you know it. The reference of default(A) isn't something that's passed around - yes the method you dispatch to has access to `this`, but what's the point of A: declaring a variable in a 'class instance' and B: using it when it's in an invalid state? It's what a moron would do. I don't call `((string)null).ToString()` because it's fucking stupid. But I assume in your world that means C# can't do method dispatch by reference?
Just because somebody can do something stupid doesn't devalue any particular technique that requires you to not do the stupid thing. Otherwise, you may as well delete C# as a language - because it's trivially easy to do stupid things. In fact software engineering wouldn't even have gotten off the ground if that was a pre-requisite.
But clearly people do produce software in it - which proves your arguments wrong.
> Showing an unsafe implementation of polymorphic return values in some other language is pointless and irrelevant.
Show me where it was mentioned in the original comment about safety? Not there is it. You just jumped in with inaccurate claims and went on some tangent about type-system safety, like C# would ever win any type-system safety contests.
Leaving asside the fact that all of your arguments about safety are nonsense for the moment... let's do it another way ...
public interface Monoid<MA, A> where MA : struct, Monoid<MA, A>
{
A Empty();
A Append(A x, A y);
}
public struct MString : Monoid<MString, string>
{
public string Append(string x, string y) => x + y;
public string Empty() => "";
}
public struct MList<A> : Monoid<MList<A>, List<A>>
{
public List<A> Append(List<A> x, List<A> y) => x.Concat(y).ToList();
public List<A> Empty() => new List<A>();
}
public static class Monoid
{
public static A Concat<MA, A>(IEnumerable<A> ma) where MA : struct, Monoid<MA, A> =>
ma.Fold(default(MA).Empty(), default(MA).Append);
}
class Program
{
static void Main(string[] args)
{
var strs = new[] { "Hello", ",", " ", "World" };
var lists = new[] { List.New(1, 2, 3), List.New(4, 5, 6) };
var str = Monoid.Concat<MString, string>(strs);
var list = Monoid.Concat<MList<int>, List<int>>(lists);
}
}
That is now safe in that `Concat` can't be implemented without the `struct` constraint, the code will fail to compile. Also the types that implement `Monoid<MA, A>` must be structs.
I'm out of this discussion now - because if you're still going to claim this is unsafe then you're clearly trolling and I haven't really got the motivation to keep feeding you.
> The reference of default(A) isn't something that's passed around - yes the method you dispatch to has access to `this`, but what's the point of A: declaring a variable in a 'class instance' and B: using it when it's in an invalid state?
It's not something you'd deliberately do, but in any decent-sized codebase, everything the language permits will happen. If it's possible to exclude a given pitfall with a simple, local lint rule then you might be able to avoid it, but manual review of anything that can happen at a distance is doomed to failure.
> I don't call `((string)null).ToString()` because it's fucking stupid. But I assume in your world that means C# can't do method dispatch by reference?
Unless you can use a very simple set of local rules to avoid having that happen, yes. Fortunately, there is such a set of rules you can follow (namely never writing null, never using constructs that return null, and checking the return values of library calls for null immediately) and so null (barely) doesn't destroy the language completely.
> Just because somebody can do something stupid doesn't devalue any particular technique that requires you to not do the stupid thing.
If your technique makes it impossible to use simple rules to avoid doing the stupid thing, then yes, that does devalue the technique. Because at that point having the stupid thing happen in your codebase is just inevitable.
> Otherwise, you may as well delete C# as a language - because it's trivially easy to do stupid things.
I already did, thanks.
> In fact software engineering wouldn't even have gotten off the ground if that was a pre-requisite.
Nonsense. Typed lambda calculi predate mechanical computers and don't allow you to do anything stupid. We could've built software engineering on them.
> But clearly people do produce software in it - which proves your arguments wrong.
People produce software in C#, but it takes more effort and has higher defect rates than doing so in Haskell-like languages.
> Show me where it was mentioned in the original comment about safety? Not there is it.
It's implicit because a) Haskell is a safe-by-default language b) return type polymorphism without safety is completely trivial. In e.g. Python you can just have Concat return "", [], or something else; likewise you can do the same in C# if you're happy to cast. So clearly moomin can't miss just being able to have a function that returns "" or [], because what language could they possibly be working in where that would be impossible or even at all difficult?
> That is now safe in that `Concat` can't be implemented without the `struct` constraint, the code will fail to compile. Also the types that implement `Monoid<MA, A>` must be structs.
But a) I have to allow "default(MA)" expressions in my program, which means I have no way to ban the unsafe use of default() b) nothing stops an implementation of Monoid<MA, A> being a struct that contains a reference, in which case that reference will be null when the struct is initialized with default(). It doesn't solve the problem at all.