8
votes

In the Haskell community, we are slowly adding features of dependent types. Dependent types is an advanced typing feature by which types can depend on values. Some languages like Agda and Idris already have them. It appears to be a very advanced feature requiring an advanced type system, until you realize that python has had dependent types has had the dynamic typing version of dependent types, which may or may not be actual dependent types, from the beginning.

For most any program in a functional programming language, there is a way to reperesent it as an untyped lambda calculus term, no matter how advanced the typing. That's because typing only eliminates programs, not enable new ones.

Strong Typing wins us safety. How classes of errors that happened at run time can no longer happen at run time. This safety is rather nice. Besides this safety though, what does strong typing give you?

Are there an additional benefits of a strong type system besides safety?

(Note that I'm not saying that strong typing is worthless. Safety is a huge benefit in and of itself. I'm just wondering if there are additional benefits.)

4
"until you realize that python has had dependent types from the beginning" ... can you elaborate? I'm not sure I've ever seen a forall type in a dynamically typed language, let alone a dependent type.chi
"Python has had dependent types from the beginning" -- eh, no. There is no tool that will look at a Python program and pronounce it ill-typed without running it.Daniel Wagner
@PyRulez You are confusing the codomain of a function with its image. Just because you can define a Python function that only returns, say, strings of length 5, the type of those values remains str, indistinguishable at the type level from strings with other lengths.chepner
@PyRulez Python achieves a high-level of metaprogramming not by promoting value-level things to the type level, but my demoting type-level things to the value level. The fact that str can be returned from a function, for example, shows that str is a type being treated like a value, not the other way around. The difference is that value-level things are not statically knowable.Elliot Cameron
@PyRulez It seems to me like the phrase "dependently typed" loses its meaning if you try to characterize it like that. All programming languages that have some amount of data hiding ability and can throw errors (at least the weakly or dynamically typed ones) would then be "dependently typed."David Young

4 Answers

27
votes

First, we need to talk a bit about the history of the simply typed lambda calculus.

There are two historical developments of the simply typed lambda calculus.

  • When Alonzo Church described the lambda calculus the types were baked in as part of the meaning / operational behavior of the terms.

  • When Haskell Curry described the lambda calculus the types were annotations put on the terms.

So we have the lambda calculus a la Church and the lambda calculus a la Curry. See https://en.wikipedia.org/wiki/Simply_typed_lambda_calculus#Intrinsic_vs._extrinsic_interpretations for more.

Ironically, the language Haskell, which is named after Curry is based on a lambda calculus a la Church!

What this means is the types aren't simply annotations that rule out bad programs for you. They can "do stuff" too. Such types don't erase without leaving residue.

This shows up in Haskell's notion of type classes, which are really why Haskell is a language a la Church.

In Haskell, when I make a function

sort :: Ord a => [a] -> [a]

We're passing an object or dictionary for Ord a as the first argument.

But you aren't forced to plumb that argument around yourself in the code, it is the job of the compiler to build that up and use it.

instance Ord Char
instance Ord Int
instance Ord a => Ord [a]

So if you go and use sort on a list of strings, which are themselves lists of chars, then this will build up the dictionary by passing the Ord Char instance through the instance for Ord a => Ord [a] to get Ord [Char], which is the same as Ord String, then you can sort a list of strings.

Calling sort above, is a lot less verbose than manually building a LexicographicComparator<List<Char>> by passing it an IComparator<Char> to its constructor and calling the function with an extra second argument, if I were to compare the complexity of calling such a sort function in Haskell to calling it in C# or Java.

This shows us that programming with types can be significantly less verbose, because mechanisms like implicits and typeclasses can infer a large part of the code for your program during type checking.

On a simpler basis, even the sizes of arguments can depend on types, unless you want to pay fairly massive costs for boxing everything in your language up so that it has a homogeneous representation.

This shows us that programming with types can be significantly more efficient, because it can use dedicated representations, rather than paying for boxed structures everywhere in your code. An int can't just be a machine integer, because it has to somehow look like everything else in the system. If you're willing to give up an order of magnitude or more worth of performance at runtime, then this may not matter to you.

Finally, once we have types "doing stuff" for us, it is often beneficial to consider the refactoring benefits that mere safety provides.

If I refactor the smaller set of code that remains, it'll rewrite all that type-class plumbing for me. It'll figure out the new ways it can rewrite the code to unbox more arguments. I'm not stuck elaborating all of this stuff by hand, I can leave these mundane tasks to the type-checker.

But even when I do change the types, I can move arguments around fairly willy-nilly, comfortable that the compiler will very likely catch my errors. Types give you "free theorems" which are like unit tests for whole classes of such errors.

On the other hand, once I lock down an API in a language like Python I'm deathly afraid of changing it, because it'll silently break at runtime for all my downstream dependencies! This leads to baroque APIs that lean heavily on easily bit-rotted keyword-arguments, and the API of something that evolves over time rarely resembles what you'd build out of the box if you had it to do over again. Consequently, even the mere safety concern has long-term impact in API design once you ever want people to build on top of your work, rather than simply replace it when it gets too unwieldy.

21
votes

That's because typing only eliminates programs, not enable new ones.

This is not a correct statement. Type-classes make it possible to generate parts of your program from type-level information.

Consider two expressions:

  1. readMaybe "15" :: Maybe Integer
  2. readMaybe "15" :: Maybe Bool

Here I'm using the readMaybe function from the Text.Read module. At term level those expressions are identical, only their type annotations are different. However, the results they produce at runtime differ (Just 15 in the first case, Nothing in the second case).

This is because the compiler generates code for you from the static type information you have. To be more precise, it selects a suitable type class instance and passes its dictionary to the polymorphic function (readMaybe in our case).

This example is simple, but there are way more complex use cases. Using the mtl library you can write computations that run in different computational contexts (aka Monads). The compiler will automatically insert a lot of code that manages the computational contexts. In a dynamically typed language, you would have no static information to make this possible.

As you can see, static typing not only cuts off incorrect programs but also writes correct ones for you.

4
votes

You need "safety" when you already know what and how you want to write. It's a very small part of what types are useful for. The most important thing about types is that they make your reasoning structured. When someone writes in Python a + b he doesn't see a and b as some abstract variables — he sees them as some numbers. Types are already there in the internal language of humans, Python just doesn't have a type system to talk about them. The actual question in the "typed vs untyped (unityped) programming" dispute is "do we want to reflect our internal structured concepts in a safe and explicit or unsafe and implicit way?". Types don't introduce new concepts — it's untyped reasoning forgets the existing ones.

When someone looks at a tree (I mean a real green one) he doesn't see every single leaf on it, but he doesn't treat it as an abstract nameless object as well. "A tree" — is an approximation that is good enough for most cases and that's why we have Hindley-Milner type systems, but sometimes you want to talk about a specific tree and you do want to look at leaves. And that's what dependent types give you: the ability to zoom. "A tree without leaves", "a tree in the forest", "a tree of a particular form"... Dependently typed programming is just another step towards how humans think.

On a less abstract note, I have a type checker for a toy dependently typed language, where all typing rules are expressed as constructors of a data type. You don't need to dive into the type checking procedure to understand the rules of the system. That's the power of "zooming": you can introduce as complex invariants as you want, thus distinguishing essential parts from not important ones.

Another example of the power dependent types give you is various forms of reflection. Look e.g. at the Pierre-Évariste Dagand thesis, which proves that

generic programming is just programming

And of course types are hints, many functions and abstractions I defined I would define in a far more clumsy way in a weakly typed language, but types suggested better alternatives.

There is just no question "What to choose: simple types or dependent types?". Dependent types are always better and they of course subsume simple types. The question is "What to choose: no types or dependent types?", but that question doesn't stand for me.

3
votes

Refactoring. By having a strong type system you can safely refactor code and have the compiler tell you whether what you are doing now even makes sense. The stronger the typing system, the more refactor errors are avoided. This of course means your code is a lot more maintainable.