4

Note: if this question is somehow odd, this is because I was only recently exposed to Haskell and am still adapting to the functional mindset.

Considering a data type like Maybe:

data MyOwnMaybe a = MyOwnNothing | MyOwnJust a

everyone using my data type will write functions like

maybeToList :: MyOwnMaybe a -> [a]
maybeToList MyOwnNothing  = []
maybeToList (MyOwnJust x) = [x]

Now, suppose that, at a later time, I wish to extend this data type

data MyOwnMaybe a = MyOwnNothing | MyOwnJust a | SuperpositionOfNothingAndJust a

how do I make sure that everyone's functions will break at compile-time?

Of course, there is the chance that somehow I'm not "getting" algebraic data types and maybe I shouldn't be doing this at all, but considering a data type Action

data Action = Reset | Send | Remove

it would seem that adding an extra Action like Add would not be so uncommon (and I wouldn't want to risk having all these functions around that possibly cannot handle my new Action)

Werner de Groot
  • 933
  • 7
  • 15
  • 1
    Is your question really “make sure that everyone's functions _will_ break at compile-time”, or how to _prevent_ braking anyone's code? (Both can make sense) – leftaroundabout Aug 30 '15 at 17:40
  • 2
    seems like a slight variation of the [expression problem](http://userpages.uni-koblenz.de/~laemmel/TheEagle/resources/pdf/xproblem1.pdf) – Random Dev Aug 30 '15 at 17:41
  • I think *both*. I really want to make sure that the program (both the part written by me, as the parts consuming my code) will function as it is supposed to. I feel like I'm running the risk of "falling through" a lot of these functions that were (obviously) not written with my new `Add` in mind. This "falling through" will cause, if I am correct, a runtime exception. This would be very undesirable. – Werner de Groot Aug 30 '15 at 17:44
  • How should code suddenly know how to act "as it is supposed to" if you add *cases*? – Random Dev Aug 30 '15 at 17:45
  • In this case, I would prefer a compile-time error so that I know I need to reconsider these functions. This would be different from the case in which I have a maybeToList _ = [] at the end. – Werner de Groot Aug 30 '15 at 17:46

3 Answers3

3

You seem to know that GHC can warn about non-exhaustive pattern matches in function via the -W flag or explicitly with -fwarn-incomplete-patterns.

There is a good discussion about why these warnings are not automatically compile-time errors at this SO question:

In Haskell, why non-exhaustive patterns are not compile-time errors?

Also, consider this case where you have an ADT with a large number of constructors:

data Alphabet = A | B | C | ... | X | Y | Z

isVowel :: Alphabet -> Bool
isVowel A = True
isVowel E = True
isVowel I = True
isVowel O = True
isVowel U = True
isVowel _ = False

A default case is used as a convenience to avoid having to write out the other 21 cases.

Now if you add an addition constructor to Alphabet, should isVowel be flagged as "incomplete"?

Community
  • 1
  • 1
ErikR
  • 51,541
  • 9
  • 73
  • 124
  • I did not know this! Is there some way I can "mark" functions that *should* be complete (without a default case) and distinguish those from functions that will work regardless of whether we add new cases or not? (whether via `-W` or though some other means) – Werner de Groot Aug 30 '15 at 17:50
  • @WernerdeGroot Not in Haskell, AFAIK. I'd _really_ love to be able to ask the compiler to treat non exhaustiveness as an error. In some other languages e.g. Coq or Agda that is the case by default. – chi Aug 30 '15 at 17:58
  • You can do it with `-Werror` but that's obviously a package deal. – MasterMastic Aug 30 '15 at 18:00
  • 1
    @MasterMastic I find `-Werror` to be too radical, since it affects all the other warnings as well. That's an option, though. I'd like something which I could add to my .cabal without worrying about newer compilers being too picky in the future. – chi Aug 30 '15 at 18:07
  • 1
    @chi, that makes good sense. Add `-ferror...` to `-fwarn...` and `-fno-warn...`. – dfeuer Aug 31 '15 at 06:28
3

Well, bad news first: sometimes you just can't do it. Period.

But that is language-agnostic; in any language you sometimes have to break interface. There is no way around it.

Now, good news: you can actually go a great length before you have to do that.

You just have to carefully consider what you export from your module. If, instead of exporting the internal workings of it, you export high-level functions, then there is a good chance you can rewrite those function using the new data type, and everything would go smooth.

In particular, be very careful when exporting data constructors. In this case, you don't just export functions that create your data; you are also exporting the possibility of pattern-matching; and that is not something that ties you pretty tight.

So, in your example, if you write functions like

myOwnNothing :: MyOwnMaybe a
myOwnJust :: a -> MyOwnMaybe a

and

fromMyOwnMaybe :: MyOwnMaybe a -> b -> (a -> b) -> b
fromMyOwnMaybe MyOwnNothing b _ = b
fromMyOwnMaybe (MyOwnJust a) _ f = f a

then it's reasonable to assume that you would be able to reimplement it for the updated MyOwnMaybe data type; so, just export those functions and the data type itself, but don't export constructors.

The only situation in which you would benefit from exporting constructors is when you are absolutely sure that your data type won't ever change. For example, Bool would always have only two (fully defined) values: True and False, it won't be extended by some FileNotFound or anything (although Edward Kmett might disagree). Ditto Maybe or [].

But the idea is more general: stay as high-level as you can.

MigMit
  • 1,698
  • 12
  • 14
  • Would you generally advise to provide access to a data type of x cases with a function which takes x functions (some of which may be just a default value, like you did for MyOwnNothing) that provide access to these x different types? – Werner de Groot Aug 31 '15 at 14:21
  • That really depends on what the intended use is. I defined `fromMyOwnMaybe` assuming that `MyOwnMaybe` would serve almost the same purpose as the regular `Maybe`. Generally you would need to consider the intended use pattern — what do you know about it, what definitely should be there and what shouldn't. There is no predefined set of rules. If you don't know anything, then yes, this `fold`-like approach might be the best. – MigMit Aug 31 '15 at 21:53
1

One thing that a lot of modules do is not to export their constructors. Instead, they export functions that can be used (“smart constructors”). If you change your ADT later, you have to fix your functions in the module, but no one else's code gets broken.

leftaroundabout
  • 117,950
  • 5
  • 174
  • 319
PyRulez
  • 10,513
  • 10
  • 42
  • 87