
Leveraging the type system to avoid mistakes - amzans
https://www.beyondthelines.net/programming/leveraging-the-type-system-to-avoid-mistakes/
======
amzans
Something that I enjoy very much in Scala is sealed traits + case classes.

    
    
      sealed trait Food
      case class FishTaco(size: FishSize) extends Food
      case class Burger(numPatties: Int) extends Food
      case class Spaghetti(sauce: PastaSauce) extends Food
    
    

It’s like Enums + Data Classes on steroids. You can pass around and transform
your data as usual, but if you forgot to handle a specific case when doing
pattern matching you’ll get a compile warning. For example in the following
case:

    
    
      input match {
        case Burger(patties) => ...
        case Spaghetti(sauce) => ...
      }
    

The compiler will happily yell at you something like:

    
    
      warning: match may not be exhaustive.
      It would fail on the following input: FishTaco(_)
    
    

Little things such as this one help reasoning about your program since you can
rely on the types describing what is possible and what is not.

~~~
Rapzid
If you like that kinda thing you should check out F# as well. The IDE
experience/speed is much better for F# IMHO, but I have a lot of respect for
the concepts behind both. And of course they ride on completely separate
massive ecosystems, so you can't exactly swap them out willy-nilly..

I wish I could figure out why my IDE experience with Scala and IntelliJ(IDEA)
was so darn sluggish. Also hoping they jump on the language server protocol
bandwagon in the future.

~~~
megaman22
I want desperately to start moving a legacy C# codebase to F#. The problem is
that I'm currently the only one that understands the C# codebase, other
developers on the project struggle to understand the JS front-end, and flail
on contact with the back-end. I'm at severe risk of making myself an Expert
Beginner indispensable piece, but I don't know how to move forward with what I
have to work with.

~~~
wjossey
Based on what you’re saying, adding greater understanding to the current
codebase is problem #1, which you’ll need to solve before going to F#.
Language migrations require two things, at minimum:

[1] A really firm understanding of the business logic the code is performing
(which can really be simplified to, do you have a deep understanding of “the
business”)

[2] An understanding of the edge cases.

#2 is often only captured in the code, and these regressions end up being the
thing that cause lots of bumps along the way with business teams. Knowledge
gets lost so easily, or silo’d in distant parts of the business, that the code
really becomes the “living history” of the business.

I don’t have a lot of good solutions for you. Migrating languages is tough
business, but I’d potentially think about starting a “lunch and learn” with
some colleagues to do high level code walkthroughs. This could help to
distribute knowledge, and build a better understanding of what you already
have. The process of prepping can help you to also document what it might take
to do the migration, so you can better make the case to any superiors you need
to get buy in from.

Good luck!

------
dnomad
Types can definitely help with data well-formedness but personally I'm very
suspicious of 'mini-types' like Name and CartID. If the data you're wrapping
has no internal structure (that can be formally verified) and you're just
using the type system as an argument checker then it's not clear to me that
you're really adding much value. If CartID and ProductID are both just strings
what's to stop a user from doing the same as:

    
    
      CartID cartId = new CartID(productId.toString());
      addToCart(..., cartId, ...); <-- but look that's really a ProductID!
    

I'd suggest that while you may have reduced the likelihood of certain errors
you have not actually eliminated those errors.

The right approach here is to be very suspicious of any methods in your domain
that just take a bunch of IDs. Either:

(a) Force the caller to resolve the IDs to actual entities before invoking the
method:

    
    
      addToCart(Cart c, Product p, ...)
    

or (b) Represent the method parameters as a Parameters object or, preferrably,
an Event object.

    
    
      handle(ProductAddedEvent event);
    

The real problem here is not the type system but a domain that is not
sufficiently protected by an Anti-Corruption Layer [1]. Unfortunately most
developers are not familiar with this kind of strategic design and languages
are not much help here. It would be interesting if there was a language that
could flag things like weak ACLs.

[1] [https://docs.microsoft.com/en-
us/azure/architecture/patterns...](https://docs.microsoft.com/en-
us/azure/architecture/patterns/anti-corruption-layer)

~~~
gcommer
> new CartID(productID.toString());

With good data modeling and safety checks, this should fail: a product id and
cart id should have some sort of distinguishing syntax (eg "P1234" vs "C1234")
and so `new CartID("P1234")` will fail.

At some point, no sorts of type system can stop incorrectly
implemented/specified logic. An even simpler example would be just passing
some constant, valid, but not semantically correct string to the CartID
constructor.

Your (a) and (b) solutions don't solve this. I could just as easily create a
meaningless Cart object, or a meaningless ProductAddedEvent the same way you
constructed that meaningless CartID.

~~~
dnomad
As I said, if your data actually has internal structure that can be verified
then the Type is meaningful. (Though that sort of prefixing introduces its own
problems and I think most IDs just end up being UUIDs or URIs or numbers.)
Otherwise you're just wrapping strings. The point here is that a Type
represents data _and_ behavior or, more formally, Types represent constraints
over a space of data and behavior. Types that don't actually do any
constraining are useless.

> I could just as easily create a meaningless Cart object, or a meaningless
> ProductAddedEvent the same way you constructed that meaningless CartID.

The problem here is not mixing up IDs. The problem is actually guaranteeing
that the pre-conditions of addToCart() have been satisfied. Here, for example,
addToCart() probably wants more than just a Cart and Product object, but a
Cart and Product objects that have been persisted. Presumably the domain has
been constrained so that you _can 't_ create meaningless objects. Otherwise
what's the point of any object? If the only valid Cart and Product objects are
those that can be retrieved from a persistent repository then, yes, this not
only solves the trivial problem of mixing up IDs, but also the more valuable
underlying problem that addToCart() is constrained to act on real, valid
(according to the domain) Cart and Product objects.

In other words, this isn't really an issue of language design. And while
there's a tendency to believe the language can solve every problems and that
design patterns are a sign of language "weakness" the reality is developers
that really want to solve these problems will have to think carefully about
their domain and apply the appropriate design patterns.

~~~
catern
>Types that don't actually do any constraining are useless.

Exactly!

I can make CartId/ProductId/WhateverId types that are just wrappers around
String. But that's not actually using the type system to enforce correct
behavior. Nothing in the type system prevents me from violating the invariants
of my application.

I would only actually be using the type system, if my types were actually
constrained so that only _valid_ CartIds, that refer to real Carts, could be
created. Then I would actually have a useful invariant, which the type system
guarantees _cannot be violated_. (inasmuch as the validation step when
creating CartIds is correct)

~~~
jpitz
But aren't you then constrained until the end of time to ensure that cart ids
and product ids are entirely disjoint? That's way too coupled for my little
brain.

------
tetha
Another interesting and, retrospectively, obvious idea I recently read about:
Have a class 'CleartextPassword' around, which overrides all string-
serialization as either a sequence of *s, or a hashcode of the password.
Suddenly the type system prevents security issues.

~~~
Rapzid
Scott Wlaschin, of fsharp for fun and profit, has written/presented[1] quite a
bit about utilizing the type system to prevent invalid states. Diff lang, but
sim concepts. It was an epiphany to me and a concept I have at least tried to
bring back to the ALGOL side where possible.

[1][https://fsharpforfunandprofit.com/series/designing-with-
type...](https://fsharpforfunandprofit.com/series/designing-with-types.html)

~~~
jackweirdy
This reminds me of an article by Foursquare about how they use Scala Phantom
Types to ensure MongoDB queries are semantically well formed:

[https://engineering.foursquare.com/going-rogue-
part-2-phanto...](https://engineering.foursquare.com/going-rogue-
part-2-phantom-types-d18ece7235e6)

------
lihaoyi
I wrote a blog post that goes into a lot more detail into some of these
techniques, in case anyone wants to dive deeper

\-
[http://www.lihaoyi.com/post/StrategicScalaStylePracticalType...](http://www.lihaoyi.com/post/StrategicScalaStylePracticalTypeSafety.html)
\-
[http://www.lihaoyi.com/post/StrategicScalaStyleDesigningData...](http://www.lihaoyi.com/post/StrategicScalaStyleDesigningDatatypes.html)

------
bgirard
I suggested a similar trick in Gecko when we were seeing too many bugs in the
rendering/async scrolling code because of mistakes when transforming between
the different unit spaces:

[https://dxr.mozilla.org/mozilla-
central/rev/4303d49c53931385...](https://dxr.mozilla.org/mozilla-
central/rev/4303d49c53931385892231969e40048f096b4d4c/layout/base/Units.h#385-465)

Transformations between units are also typed so you wont accidentally use the
wrong scale factor when transforming.

~~~
Gibbon1
Working on porting code with time units that are totally unspecified.
Sometimes it's seconds, ms, 1/256th of a second or HH:MM:SS.

Just shoot me.

~~~
insulanian
You need F#'s units of measure [1].

[1] [https://fsharpforfunandprofit.com/posts/units-of-
measure/](https://fsharpforfunandprofit.com/posts/units-of-measure/)

------
grosjona
The problem with complex type systems is that they force you to spend a lot of
your development time thinking about type structures and their relationships
(in a universal way) instead of thinking about actual logic (in a more
localized way).

For example, when you introduce third party modules into your code, sometimes
it's impossible to cleanly reconcile the type structures exposed by the module
with the type structures within your own system... You may end up with
essentially identical concepts being classified as completely different
types/classes/interfaces... It creates an unnecessary impedance mismatch
between your logic and the logic exposed by third-party modules.

This type impedance mismatch may discourage developers from using third-party
modules altogether. That might explain why dynamically typed languages like
JavaScript and Ruby have such rich third-party module ecosystems compared to
that of statically typed languages.

Unless there is a single standardized and universally accepted type system
which is fully consistent across the minds of all developers of a specific
language/platform (for all possible applications), then a type system makes no
sense at all; especially a complex one.

~~~
hardwaresofton
Yeah this is almost completely wrong IMHO, could you give some concrete
examples of when this happened to you or someone you know?

I'm writing a relatively small haskell app, and though I've had large swaths
of time where I had to think about the types I was writing exclusively, I
almost always come out of the 30 minutes or so understanding my code, and what
I'm trying to do so much better.

Forcing you to spend time thinking about type structures and their
relationships is (dare I say) the essence of programming. Programming is in
the end about transforming data for some useful end (and some side effects
along the way), and no one wants to work with 0s and 1s directly.

Integrating with third party modules in haskell is no more painful than
integrating with 3rd party modules in Java, in fact is way way way simpler,
given Java's infamous verbosity and love of enterprise patterns (tm).

~~~
yen223
Not the OP, but I remembered writing something in Haskell, and being extremely
frustrated when I chose to model something as a List.NonEmpty, because it
wasn't straightforward to pass them around to functions that expected List.

~~~
hardwaresofton
Maybe it's the stockholm syndrome talking, but this _sounds_ like a feature to
me -- The type that represents a non-empty list and one that can be empty are
distinct. If a function is a mapping of inputs to outputs, then those two
functions are (and should be treated as?) distinct, despite the fact that one
result space is clearly the subset of another.

The problem is I don't know much about the List.NonEmpty type -- it seems like
it would be almost trivial to implement it while maintaining List-
compatability, but clearly it's not (and was frustrating to use) so...

Your point is definitely true, some things like that are very frustrating to
deal with in Haskell -- especially when you expect something like that to
'just work' and it doesn't.

~~~
gizmo686
Haskell does not have OOP style inheritance. If you define a new type, it is
impossible to use it where you were expected a value from a concrete old type.

The way around this is typeclasses, which are essentially interfaces. But this
would require that the functions you are calling (which may be written by a
third party) be written against a generic typeclass instead of the concrete
List type. Haskell is improving in this regard, but there is still many places
that are written expecting a List. Also, if the function needs any beyond
simple iteration, the typeclass it would use would have a scary name like
Semigroup (even iteration uses Foldable, which might not be clear to
beginners)

There is the IsList typeclass. But the intended use for that is to enable a
list-like datatype to be represented with list literals in source code.

Even with all of these difficulties, the worst case scenario is to spam your
code with NonEmpty.toList and NonEmpty.fromList (or its safe sister
NonEmpty.nonEmpty which returns an error value instead of throwing when given
an empty list.

------
CharlesMerriam2
There is often an I love {Scala|Rust|Erland} post showing an simple type issue
and claiming that using a bunch of new syntax would somehow fix it.

<rant>

1\. I want a minimum of typed syntax for safety. For example, remember
Hungarian Notation? My linter barfs if I try "icCart = ipMyProduct" because of
the semantic type issue.

2\. I want a full set of primitives. For example, counts (c) are 0 or above,
not infinite, can do arithmetic) while ids (i) are positive, not infinite,
cannot do arithmetic, cannot assign constant). I want optionals; I want
clearer error handling; I want fully declared I/O instead of manual error
handling; I want better.

3\. I want tallies of how often simple types are confused before changing
languages to fix. My type confusions are complicated nested collections or
variant records.

</rant>

Typing is a means for correct programming, not an end in itself.

~~~
jpitz
>Typing is a means for correct programming, not an end in itself.

That's not always the case.

[https://aphyr.com/posts/342-typing-the-technical-
interview](https://aphyr.com/posts/342-typing-the-technical-interview)

------
GavinMcG
This got me thinking, and I'm curious what a language would look like if basic
types weren't directly usable.

In other words, maybe type checking doesn't go far enough: checking that I'm
getting an int is less valuable than checking that I'm getting the right sort
of data _for the domain_.

~~~
usrusr
Whenever I play with that idea in my head I get hung up on the question wether
one could go even further down that road and drop variable names as a concept
separate from types. If you already have a Surname instead of a String and a
Birthday instead of a Date, why duplicate that into surname and bday? In a
way, variable names are a bit like informal domain subtypes. Could we make
them formal?

I'm really not sure if that could be in any way practical at all, but I like
the _idea_.

~~~
rbonvall
In some languages you don't give field names, just types. You bind fields to
names only when pattern matching. For example in Haskell:

    
    
        type Year = Int
        type Month = Int
        type Day = Int
    
        -- fields have no names, just types
        data Date = Date Year Month Day
    
        -- we bind fields to names only when necessary
        next (Date y 12 31) = Date (y + 1) 1 1
        next (Date y m d)
          | d == daysOf m = Date y (m + 1) 1
          | otherwise     = Date y m (d + 1)
          where daysOf = (!!) [0, 31, 28, 31, 30,
                                  31, 30, 31, 31,
                                  30, 31, 30, 31]
    

Is something like this what you mean?

~~~
usrusr
Thank you, good to know.

Maybe those who don't know Haskell are doomed to reinvent isolated parts of
it.

~~~
Iceland_jack
( yes )

------
oherrala
I wrote about the same topic in

[https://medium.com/sensorfu/using-static-typing-to-
protect-a...](https://medium.com/sensorfu/using-static-typing-to-protect-
against-code-injection-attacks-353002ca6f2b)

Type systems are really good in helping to avoid this problem!

------
platz
Make illegeal states unrepresentable

------
dfan
I have a system set up in C++ to make it trivial to add new unique kinds of
IDs (wrappers around ints) as well as vectors that can be indexed only with
that kind of ID. So you can have a ShoeVec containing Shoes that is indexable
only by ShoeIDs and a ShirtVec containing Shirts that is indexable only by
ShirtIDs. It's zero-overhead in optimized builds, of course. You'd be
surprised how rarely you have to convert to or from the underlying type.

~~~
jdonaldson
Fwiw the Haxe language calls this an "abstract type", and they're very useful.

[https://haxe.org/manual/types-abstract.html](https://haxe.org/manual/types-
abstract.html)

~~~
rurban
Generic types are useful, but still should be called as such, or for the new
fad: dependent types. Abstract types would not allow instantiation.

~~~
jdonaldson
Haxe abstract types are distinct from abstract classes, but I need to learn
more about dependent types.

------
hzhou321
The suggested type system adds a lot of code and complexity. For the specific
case, I suggest we can try a convention-based solution. Let's, eg. establish a
convention that all product id variable named with `prod_` prefix and customer
id variable named with `cust_` prefix. Then a simple static analyzer filter
can serve as the unit test to catch mistakes (include passing in variable
names that don't conform the convention). We should recognize that the
enforced prefix serves the same role as the type system but at the
preprocessing stage rather than the compiler stage. It does not require the
programmer to add any extra code; rather it helps one of the difficult
problems of naming things. Unlike the type system, it is easy to adopt strong
convention (the strongest being a set of fixed names) and then relax the
convention as the program evolves. In contrast, the type system often takes
the opposite path of from relax to strict with steep increasing cost. Of
course, unlike the type system, the enforcement of the convention does not
come automatically -- the programmer has to write static analysis code to
enforce it -- this is fundamentally not different from writing unit tests. But
the static analysis code is easy to write and less likely to have bugs (and
bugs have less severe consequences). I use a general purpose preprocessor,
MyDef, and every line of my code goes through several places of filtering
anyway, so adding a set of static checks seem trivial. But even you don't use
a preprocessor, implementing a simple static convention checker (to be
enforced at repository check-in) doesn't seem difficult.

~~~
nine_k
> _unlike the type system, the enforcement of the convention does not come
> automatically_

This defeats the whole purpose.

> _the programmer has to write static analysis code_

It is legwork that should be relegated to the computer. And, in fact, has been
for a long time. I don't only mean type systems; various linters and checkers
work where the compiler proper does not (see valgrind or findbugs). Frankly, a
lot of test cases for code can be written automatically; the prime example is
Haskell's QuickCheck, but a number of other languages has similar tools, all
the way down to fully dynamic ones like Python.

~~~
hzhou321
Creating and maintain type system is also leg walk (and I was arguing it is a
bigger burden of leg walk). And a Turing complete type system can have Turing
complete errors. Let's not pretend we have silver bullet and have a fair
discussion

------
billfruit
One thing that I personally would find useful is a F# like Unit of Measure,
kind of type metadata, that allows units of quantities to be explicitly
mentioned, and there by help in preventing unit confusions in code, ie passing
around values in the wrong kind of units, esp degrees and radians for eg, can
often be a difficult mistake to catch.

------
hardwaresofton
If you like this, you're probably also going to like Haskell/OCaml/Ada/F#/Coq
(ML family of languages?):

I'll leave links for Haskell since the only one of those I've tried and liked
personally:

[https://www.haskell.org/](https://www.haskell.org/)

[http://learnyouahaskell.com/](http://learnyouahaskell.com/)

IMO Haskell is the type system you wanted Java to have, with even more
strictness. While it's possible to write unsafe haskell, just about everything
you will read, and the language itself encourages you to push as many worries
as you can to be solved by the type system.

If that doesn't get you excited, there's actually a saying that is mostly true
in my experience writing Haskell -- "If it compiles, it works"(tm).

Also Haskell on the JVM if you want to dip your toes in:

\- [https://eta-lang.org/](https://eta-lang.org/)

\- [https://github.com/Frege/frege](https://github.com/Frege/frege)

[EDIT] - If you try to learn haskell you'll inevitably run into the M word
which rhymes with Gonad. The best guide I've ever read is @
[http://adit.io/posts/2013-04-17-functors,_applicatives,_and_...](http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html)

BTW, be ye warned: when you start understanding/getting cozy with the concepts
introduced by Haskell you'll wish they were everywhere, and they're not.
Languages without proper union types will look dumb to you, easy function
composition/points free style will be a daily desire, and monadic composition
of a bunch of functions will be frustratingly absent/ugly in other languages,
not doing errors-as-values will look ironically error-prone.

[EDIT 2] - More relevant to the actual post, how you would do this in Haskell
is using the `type` keyword (type aliasing, e.g. `type Email = String`) or the
`newtype` keyword, which creates a new type that is basically identical (what
the article talks about). Here's some discussion on newtype:

\- [http://degoes.net/articles/newtypes-
suck](http://degoes.net/articles/newtypes-suck) (this article is good, it
describes the premise, promise, shortcomings, and workarounds, forgive the
clickbaity title)

\-
[https://www.reddit.com/r/haskell/comments/4jnwjg/why_use_new...](https://www.reddit.com/r/haskell/comments/4jnwjg/why_use_newtype/)

~~~
Silhouette
Although Haskell has a relatively powerful and useful type system, you are
perhaps being a little kind to it here. Haskell also has partial functions,
including quite a few dangerous examples in the standard library, various
mechanisms to escape the type system entirely, and 27 different ways to handle
errors, 28 of which look a bit like exceptions but need handling in 29
different ways. And it still can't handle basic record types very elegantly,
nor dependent types such as fixed-size arrays/matrices. Haskell's type system
does have a lot going for it and it also makes a useful laboratory for
experiments with type-based techniques, but as far as safety goes it has never
really lived up to the hype, and for all its advanced magic, it is
surprisingly lacking in support for some basic features that are widely
available in other languages.

~~~
hardwaresofton
You are absolutely right -- there are other ML-family languages that do a
better job at avoiding some of the pitfalls (but surely with other tradeoffs).
Most of the stuff you've mentioned is actively recognized by the Haskell
community and has been somewhat improved:

> Haskell also has partial functions, including quite a few dangerous examples
> in the standard library,

Partial functions ->
[https://wiki.haskell.org/Avoiding_partial_functions](https://wiki.haskell.org/Avoiding_partial_functions)
(introduces the safe library @
[https://hackage.haskell.org/package/safe](https://hackage.haskell.org/package/safe))

> various mechanisms to escape the type system entirely

Type systems are leaky abstractions. While that's just a random assertion with
no fact to back it up, I think it is a good idea design wise to account for
that possibility, and give people the ability to completely bin the type
system when they need to. Most of that stuff is marked with some wording that
strongly suggests against use, but does not prohibit (e.g. unsafePerformIO)

> 27 different ways to handle errors

Could you elaborate? I understand this is hyperbole, but I feel like the
number in actuality is <= 3... which might make your statement ~90% hyperbole,
which is a bit much.

> still can't handle basic record types very elegantly

Compared to which language? Records in Haskell are indeed a PITA to get values
out of when they're deeply nested, and you have to destructure or string to
gether the accessor functions or something, but I still like it more than
other languages which take a looser approach, often leading to runtime errors.
If I do a obj.something.somethingElse, that line might blow up at runtime, but
in Haskell I have some guarantees/checking that it won't. C#'s `?.` operator
is pretty good for accessors when dealing with nullable values as well.

The lens library helps with that, but though it's simple in function, the
mathematics around it that are often introduced at the same time complicate
things.

> nor dependent types

[https://www.reddit.com/r/haskell/comments/8cp2zg/whats_the_s...](https://www.reddit.com/r/haskell/comments/8cp2zg/whats_the_status_on_dependent_types_in_ghc_as_of/)

> laboratory for experiments with type-based techniques

Haskell is less of a laboratory these days, people are actually using it for
production stuff now, and they can't change core pieces of the languages so
quickly -- I'm sure people would love to go back and eliminate partial
functions from the standard library where reasonable.

Languages like Ada might be better for experimental type stuff?

> never really lived up to the hype

Compared to what? Would you consider Haskell better for type safety than say
Java/C#? Because I certainly do (I'd love to be proven wrong as well).

I assume you meant the features you noted above as missing (easy record types,
coherent error handling, lack of partial functions) in Haskell but widely
available in other languages -- could you note which ones you had in mind?
Java is what I've seen used most in enterprise and every function is basically
a huge walking partial function -- Haskell's type system (at the very least)
seems like a huge step up there. Things got better with 1.8 and the
introduction of Optional (and are probably even better these days) but back
when I wrote a lot of Java, it didn't seem right to 'poison' all the code with
Optional<>s everywhere... Now I know better, I wasn't poisioning the code, I
was writing better code but couldn't recognize it at the time.

Contrast this with Java that waited until 1.8 for functions as a first class
object.

~~~
gizmo686
>> 27 different ways to handle errors

> Could you elaborate? I understand this is hyperbole, but I feel like the
> number in actuality is <= 3

Not quite 27, but exceptions in Haskell are quite messy. You have:

1) Exceptions. Which can be thrown from pure code, but only caught in IO. And
which get thrown when evaluated, which causes a mess with Haskell's lazy
evaluation. So the following code is not safe:

    
    
        val <- catch (willThrowError) handler
        print val
    

Because willThrowError may return a thunk that will not throw an error until
you attempt to resolve it to a value. This means that it will not actually
throw an error until `print val`, at which point it is outside of the catch,
so your entire function will error, and your handler will not be called. To be
safe, you want to do something like:

    
    
        val <- catch (evaluate . force <$> willThrowError) handler
        print val
    

Which requires the return value of willThrowError to be a support type. With
the proper language extensions, it is not difficult to autogenerate the
instance needed for a type to work with _force_ , so long as you control the
type definition and all the types it relies on supports it. I avoid actual
Exceptions like the plauge, so I might be off on some details here.

2) Maybe/MaybeT/Either/EitherT

Arguably 4 methods, but I will concede that they are the same-ish.

3) ExceptT.

Maybe belongs in catagory 2?

4) MonadError

5) MonadFail

I might be missing somne.

~~~
hardwaresofton
Hey thanks for clarifying -- Maybe I've just been a victim too long, but those
actually don't seem like distinct ways. As far as why you can throw an
exception in pure code but only catch it in monad, much ink has been
spilled[0]. I won't claim to completely understand the reasoning in that SO
post and the linked paper[1], but the reasons for this dichotomy seem to be
clear.

2) -> 5) are not ways of handling errors though, they're ways of
characterizing/combining monadic computations that CAN error, IMO. In the end,
it's still the errors-as-values (`Either SomeException value`) concept, but in
combined with the fact that your computation is in a monadic context. Also,
maybe I've just been brainwashed by myself to think that way after doing
Haskell for this short amount of time.

In practice, how I throw an error from a monadic computation (I generally have
never done it from pure code) has been pretty well-defined, I haven't had to
go down this particular rabit hole much at all -- ExceptT is what I've used
the most (I've done most of my work with Servant[2]). Honestly what I've had
problems the most with from my haskell code was dealing with the database or
mail servers and tcp sockets and stuff -- stuff that failed at runtime because
I put in some bad code Haskell couldn't check before runtime.

That said, monads & monad transformers are indeed a complex concept, and are
very confusing to beginners (I'm still not 100% confident in my knowledge of
them), so this is definitely a point against Haskell.

And of course your earlier point about unsafe functions in the std lib is as
valid as ever.

[0]: [https://stackoverflow.com/questions/12335245/why-is-
catching...](https://stackoverflow.com/questions/12335245/why-is-catching-an-
exception-non-pure-but-throwing-an-exception-is-pure#12345665)

[1]: [https://www.microsoft.com/en-
us/research/publication/tacklin...](https://www.microsoft.com/en-
us/research/publication/tackling-awkward-squad-monadic-inputoutput-
concurrency-exceptions-foreign-language-calls-
haskell/?from=http%3A%2F%2Fresearch.microsoft.com%2Fen-
us%2Fum%2Fpeople%2Fsimonpj%2Fpapers%2Fmarktoberdorf%2F)

[2]: [https://haskell-servant.readthedocs.io/](https://haskell-
servant.readthedocs.io/)

~~~
gizmo686
My only real complaint about exception handling in Haskell is the interaction
with laziness. 90% of my problems would go away if they just made the default
catch functions include `evaluate . force` and save the lazy behavior for
special "I know what I'm doing" functions. Without that, exceptions are just a
loaded gun. Even with making exceptions "safe", I still much prefer the error-
as-values approach. Otherwise, every pure function is really some type of
ExceptT IO monad. I'm sure this escape hatch is useful to someone, but
actually using it is a pretty big code smell.

~~~
danharaj
We basically ban throwing exceptions in pure code at my shop; usually we use
ExceptT or throwIO or something.

I used to think the multiplicity of error handling constructs in Haskell was
confused and bad, but they all have their uses. The key insight is that your
error handling should conform to the local structure of your code, not the
other way around.

Propagating errors across multiple component boundaries or multiple layers can
be cumbersome but I think that reveals fundamental architectural weaknesses in
the program itself.

------
andreygrehov
I believe a lot of similar concepts are explained in Domain Driven Design.

------
dclowd9901
What really put me on to typing JavaScript was this. The ability to avoid unit
tests. And frankly what unit test would ever even capture something like this,
an implementation issue, (unless you refactored a lib).

~~~
clintonb
> Don’t get me wrong I’m not saying to scrap all your test suites and try to
> encode all your constraints using the type system instead. Not at all I
> still firmly believe that tests are useful but in some cases you can
> leverage the type system to avoid mistakes.

You should still write unit tests. You should also write
integration/functional tests that might have caught this error when you tested
the code that calls the function in question. The only scenario I can see the
argument swapping bug not being caught is when all of the ID arguments equal
the same value.

------
49bc
I’m seeing a little bit of a waterbed effect here. The author has replaced the
basic type with one that matches it in name. You will never accidentally send
CustomerId to CartID because the compiler will fail... but now we’ll nees to
dig through the code to find out whether customer ID should be an int, int32,
String.... what the heck is it anyway?

In scala can I not just say foo(item1=item1) even if foo takes only 1 arg?

~~~
amzans
Usually you would lookup the definition of those types just as you do for the
function that you are trying to call. At least it’s a good idea to know what a
function accepts before passing around your customer IDs ;)

And to answer your last question, yes in Scala you can use named parameters
like in your example.

------
rurban
The id type parameter is not called phantom type, it's called generic type.

------
tzahola
Even though people love bashing Objective-C for its “weird” method syntax, it
trivially circumvents these kind of problems.

Also related: Mike Ash’s post about using single-member C structs instead of
unitless scalar types: [https://www.mikeash.com/pyblog/friday-
qa-2013-08-02-type-saf...](https://www.mikeash.com/pyblog/friday-
qa-2013-08-02-type-safe-scalars-with-single-field-structs.html)

------
thankthunk
Leverage the type system? Type systems exists to allow you catch errors at
compile time. That is what it's there to do. It's like saying leveraging the
compiler to compile or leveraging the cup to drink water.

------
jph
The article's example boils down to this pseudocode:

    
    
        function foo(s, i) { s * i }
        s = "hello"
        i = 2
        foo(i, s) // BUG. The runtime crashes.
    

The article advocates adding a type system to detect the bug:

    
    
        function foo(String str, Integer int) { s * i }
        String s = "hello"
        Integer i = 2
        foo(i, s) // BUG. The compiler detects the error.
    

If you're interested in these kinds of errors, then you may also want to
consider ideas such as named parameters:

    
    
        function foo(text: s, count: i) { s * i }

~~~
amzans
The problem with named parameters is that you can theoretically still pass the
wrong value, and in practice not everyone is using named parameters on every
function call.

By relying on the type system you can catch that issue at compile time and not
have to worry about it.

We use this technique at work when there’s lots of fields in our data types as
it’s nice to be able to keep track of the types during all the transformations
and to avoid subtle bugs. Specially as the codebase is constantly changing.

~~~
tayo42
Do you do this for every string, int, float etc..?

Any downsides you run into? I always thought it would be interesting to try it
using a type even if it wraps something but never had tried going all in on
the idea.

In scala do you not use alias ever? I guess they don't help much? Alias of
strings can be used interchangeably I think?

~~~
clausok
I've found there are situations where primitives are fine and wrapping doesn't
add much value, e.g., (using F#)

    
    
      let calcCorrelation (symbol1:string) (symbol2:string) = ...
    

Here, calling the function with misordered arguments won't change the result,
so the wrapping\unwrapping friction doesn't have much ROI.

but here, where 'beta' is the slope of a least-squares fitted line:

    
    
      let calcBeta (stockSym:string) (indexSym:string) = ... 
    

that's a mistake waiting to happen, because if you accidentally flip the
arguments when you call the function you get a different number. Better to
have:

    
    
      type StockSymbol = StockSymbol of string
      type IndexSymbol = IndexSymbol of string 
      let calcBeta (IndexSymbol index) (StockSymbol stock) = ...  
    

then call the function like this:

    
    
      let beta = calcBeta (IndexSymbol "SPX") (StockSymbol "AMZN")
    

Of course you could still make a mistake:

    
    
      let beta = calcBeta (IndexSymbol "AMZN") (StockSymbol "SPX") 
    

but that would stick out like a sore thumb.

