Home > Back-end >  A new type for all a (normal value) and nothing in Haskell?
A new type for all a (normal value) and nothing in Haskell?

Time:02-22

I want to

  1. create a new type and value that indicates none like Nothing of Maybe.
  2. create a new union type for all a (normal value) and none
  3. create a function that accepts the new union type, then if the argument is the none does nothing.

Without using Maybe, how can I do this in Haskell?

In Typescript

  const none = { undefined };
  type None = typeof none;
  type Option<T> = None | T;
 
  type f = <T>(a:Option<T>) => any 
  const f:f = a =>
    a === none
      ? undefined
      : console.log(a);

How to do this in Haskell? and just in case, please note here I'm not asking how to use Maybe Monad. Thanks.

CodePudding user response:

If you have an actual programming problem you're trying to solve which requires you to represent the concept of "either a value of type T or nothing", then Haskell's answer to that is to use Maybe T.

Whatever the goal of your code, if in Typescript you would use your Option to meat that goal, in Haskell you can use Maybe to meet that goal. The code will be a little different, but that's unsurprising because they are different languages.

But if you are trying to directly use the concepts involved in your Option from Typescript, in Haskell, to implement a type that is exactly the same for its own sake (rather than as a tool to solve a problem), then you're out of luck. Haskell does not have the concept of an (undiscriminated) union type, nor the means to easily simulate one1.

You could sort-of try with Typeable:

import Data.Typeable (Typeable, cast)

data None = None

f :: (Typeable a, Show a) => a -> IO ()
f x = case (cast x :: Maybe None)
        of Just None -> pure ()
           Nothing -> print x

When we've got a Typeable constraint in play we can actually check if a value is of a certain type (and if so get a reference to it where the reference has the right type, so we can use it as that type). But this forces us to go via Maybe anyway, because cast needs a way to represent the value of a cast that didn't succeed!

It's also warty because now an actual argument of our None type can't be represented at all, whereas the Maybe option can represent Just Nothing just fine. It's true that you don't often need this "nested failure-or-absence" capability (though by no means never; think of a query that might not return a result: if you need to distinguish between the failure to run the query vs the query returning no result, that's quite naturally Maybe (Maybe Result)). But I prefer my facilities that handle any type to handle any type, with uniformity. You can't get caught out by odd corner cases if there aren't any corner cases, by design.

And you can't use Typeable more generally to actually declare a type that could be the union of two specific types; you have to accept a type variable a with a Typeable constraint, which means people can pass you any type, not just the two you were trying to put into a union. Since you can only write a finite number of casts, Typeable fundamentally can only be used to handle a set of particular types specially, or a code path for anything else (which might be to error out).

As a more general point, you need to avoid over-using Typeable in order to write code that gains all the benefits of Haskell's type system. Lots of Haskell code makes use of properties that arise from what you can't do with a polymorphic type, because you don't know which concrete type it is. All of those go out the window when Typeable is in play. For a very simple example, there's the classic argument that there's only one function of type a -> a (that doesn't bottom out); with no way to know what the type a is, the function's implementation can't create one out of nothing, so it can only return it unmodified. But a function types Typeable a => a -> a could have any number of oddball rules encoded like "if the argument is an Integer, add one to it". No matter what is hiding in the implementation it won't be reflected in the type beyond the Typeable constraint, so you have to be familiar with the particulars of the implementation to work with this function, and the compiler won't spot if you make a mistake. A clear example of this effect is that in my attempt to write f above, we end up with the type f :: Typeable a => a -> IO (); there is zero indication of what types it is expecting to work with (that it wants to handle "either None or anything else").

As a totally different track, you can do this of course:

data None = None
data Option a = Some a | None' None

Now you've "created a new type to represent nothing" and "create a new union type for all a (normal value) and none". But Haskell's ADTs are discrimated unions, meaning there will always be a constructor tag (both at runtime and in your source code) on each variant in the union.

So there's really no point in having the separate None type, since once you've seen the None' tag in an Option a value you already know everything there is to know about the value; looking inside the None value to see that it is None tells you nothing2. So you might as well just use:

data Option a = Some a | None

Which is exactly the same as the built in Maybe, differing only in the names chosen.

Similarly you can obviously write a function with a case to handle when an Option is None, and a case to handle when there is something there (either of which can "do nothing", if you're returning a type where "doing something" makes any sense, i.e. some sort of action, like IO or State).

f :: Show a => Option a -> IO a
f None = pure ()
f (Some a) = print a

The code is slightly different, even ignoring for trivial syntax and naming issues; we had to reference the Some constructor and call print on the value inside it, rather than printing the argument to f directly3. And Haskell uses a typeclass divide types into ones that can meaningfully be printed and ones that can't, and only allow you to call print on the former. But it's so close that I have no reservations saying "this is the equivalent Haskell to the Typescript you wrote".

And given that Maybe already exists you might as well use it. (Similarly, should you ever need it the built in type () is - apart from the name - identical to data None = None.) But again: that is what you do if you are trying to solve a problem that you would use Option for in Typescript; if instead your goal is to implement something exactly the same as Option, then you simply can't with the tools Haskell gives you.


1 You can probably hack something truly horrible together using unsafe features (like unsafeCoerceing to and from Any). I do not know exactly how to go about making that usable and reliable, and doing so is an utter waste of time for any practical purpose; you would never use such code in a real program, it would just be an interesting exercise in how far you can hack the language implementation. So I'm not going to write an answer that addresses that angle.


2 Well, technically it sells you that the computation that produced it terminated; it could have been bottom. But you can't do anything with that information since it's impossible to test whether it was bottom; if it is you'll just error out (or not terminate) as well.


3 Printing the whole argument to f would have also worked, it would have just printed e.g. Some "value", which I assume is not what you meant.

  • Related