
Return type polymorphism in Haskell - osopanda
https://eli.thegreenplace.net/2018/return-type-polymorphism-in-haskell/
======
ekidd
Yeah, this is a really great Haskell feature, and it often feels like magic
the first time you see it. It's key to a whole lot of useful Haskell idioms.

Well-known languages which do this include Haskell and Rust (where it's also
useful). In Rust, you can write things like:

    
    
        // Generate an appropriate default value for a type.
        // Here, this will create an empty vector of String.
        let my_vec: Vec<String> = Default::default();
        
        // Parse JSON into the specified type.
        let numbers: Vec<u32> = serde_json::from_str("[1, 2, 3]")?;
    

I think there _may_ be ways to do something similar with C++ template
metaprogramming and partial specialization, and I wouldn't be surprised if
some of the ML family supported something similar using "modules". (But I
don't really understand ML modules, so don't quote me on that.)

It's a great feature, and I would love for more strongly-typed languages to
support it.

~~~
dmytrish
Yes, it feels great to just drop `read` into a context with enough type
information to infer the return type.

On the other hand, when the type context is not sufficient, it gets a bit
bulky:

    
    
       -- with sufficient type context:
       xs <- map read <$> lines <$> getContents
    
       -- an ugly inline type signature:
       xs <- map (read :: String -> Int) <$> lines <$> getContents
    

For many such calls it's good to monomorphize:

    
    
       readInt :: String -> Int
       readInt = read
    

but it still does not feel elegant (what about other return types?).

Edit: obviously, this must have been `xs <-`, not `let xs =`.

~~~
erikpukinskis
Is magic worth anything if it doesn’t work on 100% of its surface?

Isn’t “use magic, except in cases where the magical internals don’t apply—then
be explicit” a strictly worse API than “just be explicit”?

~~~
dmytrish
Return type polymorphism has usecases where it is irreplaceable (see other
comment threads).

I am just pointing out an example of bad use, which is not a problem of the
language, just of this specific API.

As for APIs in general, sometimes they do need some leeway for future
extension.

------
fphilipe
Swift supports this. Having said that, I don't think I've encountered it in
the standard library.

I recently started using the Decodable protocol and was surprised to find out
that the `decode` methods all take an explicit type, e.g.
`KeyedDecodingContainer.decode(Double.Type, forKey:
KeyedDecodingContainer.Key)`. There's a method for each supported type. [1]

Not sure why they didn't just have one method to rule them all in the public
interface, like this:

    
    
        extension KeyedDecodingContainer {
            public func decode<T: Decodable>(key: KeyedDecodingContainer.Key) throws -> T {
                return try decode(T.self, forKey: key)
            }
        }
    

The usage then becomes much simpler (assume `name` is a property of type
`String`):

    
    
        // Before:
        name = try container.decode(String.self, forKey: .name)
        // After:
        name = try container.decode(key: .name)
    

[1]:
[https://developer.apple.com/documentation/swift/keyeddecodin...](https://developer.apple.com/documentation/swift/keyeddecodingcontainer)

------
moomin
Once you've got return type polymorphism, you really start to miss it in other
languages. The simplest example possible is "mempty"

mempty :: a

gets the "default" value of a. Which makes no sense in a language where you
need an instance to have polymorphism.

(This, incidentally, is also why all OO serialization libraries are awful.)

~~~
louthy
Not sure if I exactly follow, but this is an implementation of Monoid in C#.
The interface can be seen as the type-class definition. The structs are the
equivalent of class instances.

If you look at the `static class Monoid` then you can see a general
implementation of mconcat which returns an A and works with Empty and Append.

The Program at the end shows it in use with List and String types.

    
    
        public interface Monoid<A>
        {
            A Empty();
            A Append(A x, A y);
        }
    
        public struct MString : Monoid<string>
        {
            public string Append(string x, string y) => x + y;
            public string Empty() => "";
        }
    
        public struct MList<A> : Monoid<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 List
        {
            public static S Fold<S, A>(this IEnumerable<A> ma, S state, Func<S, A, S> f)
            {
                foreach(var a in ma)
                {
                    state = f(state, a);
                }
                return state;
            }
    
            public static List<A> New<A>(params A[] items) => new List<A>(items);
        }
    
        public static class Monoid
        {
            public static A Concat<MA, A>(IEnumerable<A> ma) where MA : struct, Monoid<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);
            }
        }
    

Obviously it's not as elegant as Haskell, but does this not fit your
requirement?

~~~
lmm
This uses an unsafe "default(MA)" construct to hack around the type system,
right? There's no way to write code like this and not have your code fail with
NPEs at runtime except for manually inspecting every "default(...)" call to
check that it's called on the right kind of type.

~~~
louthy
Wrong, MA is constrained to struct; and structs can’t be null

~~~
lmm
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>.

~~~
louthy
Yes, it’s possible for programmers to write bugs.

~~~
lmm
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".

~~~
louthy
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.

~~~
lmm
> 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.

~~~
louthy
> 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...

[1]
[https://github.com/MattWindsor91/roslyn/blob/master/concepts...](https://github.com/MattWindsor91/roslyn/blob/master/concepts/docs/csconcepts.md)

~~~
lmm
> 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.

~~~
louthy
> 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?

~~~
lmm
> 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.

~~~
louthy
> 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.

~~~
lmm
> 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.

------
maxxxxx
Stuff like this makes me wonder why: a) Haskell isn't used more widely b)
isn't taught in school more. It seems it's a language that's intellectually
stimulating and shows how much a good language design can help with programing
work.

~~~
tombert
This is just my opinion, but as someone who has actually worked full-time with
Haskell (though admittedly not since 2015), I think a lot of the problem is
due to Haskell's _awful_ error messages and, for most of its life, awful
tooling.

When a newbie starts using Haskell, they are immediately put off by the fact
that the messages are...esoteric to say the least; weird things of `result a0,
expected [b0]`, can be offputting, and make things like F# a much more
attractive option.

Also, I feel like, until Intero, editor integration with ghcmod was incredibly
difficult to set up, and fairly easy to break, to a point where I eventually
uninstalled it.

Cabal, while certainly interesting, would end up in weird scenarios where
dependencies wouldn't resolve correctly, resulting in "cabal hell".

Stack has greatly improved on all of these points, and hopefully Haskell
catches on a bit more, because despite my criticisms, I do think it's a
fantastic language and platform.

~~~
Tehnix
Definitely the tooling is still a big problem, but there has been great
improvements lately!

\- HIE (Haskell IDE Engine) finally got some steam behind it, and has been
chugging along for some time now, being very usable and targeting LSP!

\- Intero (as you mention)

\- ghcid is a nice lightweight alternative

I personally feel stack + hie is a very good setup, and can be used in VSCode,
Atom, Neovim and emacs via LSP clients.

Error messages are slowly improving. GHC 8.2 got nice highlighting of the
location of the error and some colouring, bringing it just a little further. I
personally like how nice Elm is, but then again it has the advantage of a
_much_ simpler type systems, which significantly simplifies error handling.

~~~
tombert
You just highlighted how out of practice I am with Haskell...I need to fix
that! I think I will play with HIE this weekend.

I definitely think Stack is excellent, and _greatly_ reduces the cost-of-entry
for new devs, and it's only getting better, so Haskell is far from hopeless.

------
elihu
Minor clarification that doesn't pertain to the general premise of the
article:

> However, if we want to use it on user-defined types, we'll need to implement
> the Ord class manually:

For user-defined types, you can usually add "deriving Ord" to the definition,
and the compiler creates a default implementation for you. You might want to
implement it manually if the specifics of how it works are important to the
application or if the type contains a field that GHC can't automatically
derive Ord for (like a function).

Usually, for most types I use "deriving (Eq, Ord, Show)" unless I have a good
reason not to.

------
bjz_
This is one of the big things I miss from implementations of ad-hoc
polymorphism in dynamically typed languages - for example protocols in Elixir
or Clojure.

------
atilaneves
"This is a fairly unique and cool aspect of Haskell, especially if you come
from the C++ world".

That's not true at all - what Haskell has here that C++ or D don't is type
inference so that explicit type annotations aren't needed. The read function
in C++ would be

    
    
        #include <string>
        #include <iostream>
    
        using namespace std;
    
        template<typename T>
        T read(const string& str);
    
        template<>
        int read(const string& str) {
            return stoi(str);
        }
    
        template<>
        double read(const string& str) {
            return stod(str);
        }
    
        template<>
        string read(const string& str) {
            return str;
        }
    
        int main() {
            cout << read<int>("2") << endl;
            cout << read<double>("3.14") << endl;
            cout << read<string>("foobar") << endl;
        }

~~~
danharaj
The article doesn't cover this but haskell's return type polymorphism is
strictly more powerful because it can be used in cases where you don't know
the type at compile time. Such a situation can occur with recursion on nested
data types or with higher rank types.

Now, i bet with some hackery one could emulate this too, but I don't expect it
to be legible.

------
jwatte
As a previous C++ and Erlang programmer, polymorphism on return type was the
hardest thing for me to internalize. Like, I could repeat the words, but I
didn't "grok" it for the longest time. I wish there was some standard way of
emphasizing this. (Might help with the Monad tutorial jungle, too, as that's
the implementation linchpin)

------
tiziano88
I think this is just regular type unification in Hindley-Milner type systems?

~~~
z1mm32m4n
It’s more than just HM type inference. This relies on particular on ad-hoc
polymorphism (many languages with HM type inference only have parametric
polymorphism).

------
hansbo
Is it possible to write functions using this, without also using the
polymorphic functions already in place in Haskell?

~~~
tom_mellior
The library functions used in this article are all written in Haskell, not
using any particular magic. So yes, it was possible to wrote those functions,
and it's also possible to write your own. Return type polymorphism is just as
accessible to "normal" Haskell programmers as to the people who implemented
its standard libraries. It's not an internal trick of the implementation the
way some other functions (for IO, in particular) are.

~~~
hansbo
As far as I can tell, however, the article gives no examples of this. All the
functions that are written are based on "read" and "mempty" and similar. I'm
having issues understanding how to write these functions "from scratch".

~~~
masklinn
* define a typeclass with a function generic in its output

* implement the typeclass for relevant types

* invoke the typeclass function in disambiguated contexts
    
    
        class Zero z where
          zero :: z
    
        instance Zero Int where
          zero = 0
    
        instance Zero Float where
          zero = 0.0
    
        instance Zero [a] where
          zero = []
    

example usage:

    
    
        let v :: Int; v = zero
    

or

    
    
        let v = zero :: Int
    

or pass it to something which expects an Int, disambiguating it, e.g.

    
    
        > Data.Char.chr zero
        '\NUL'

------
strictfp
Very impressed by Eli's site. Has taught me a lot and introduced me to many
interesting subjects.

