9

While curiosing on the main page of a scripting programming language's site, I encountered this passage:

When a system gets too big to keep in your head, you can add static types.

This made me remember that in many religious wars between static, compiled languages (like Java) and dynamic, interpreted languages (mainly Python because it's more used, but it's an "issue" shared between most scripting languages), one of the complaints of statically typed languages' fans over dynamically typed languages is that they don't scale well to bigger projects because "one day, you'll forget the return type of a function and you'll have to look it up, while with statically typed languages everything is explicitly declared".

I never understood statements like this one. To be honest, even if you declare the return type of a function, you can and will forget it after you've written many lines of code, and you will still have to return to the line in which it's declared using the search function of your text editor to check it.

As an addition, as functions are declared with type funcname()..., without knowing type you will have to search over each line in which the function is called, because you only know funcname, while in Python and the like you coud just search for def funcname or function funcname which only happens once, at the declaration.

More over, with REPLs it's trivial to test a function for it's return type with different inputs, while with statically typed languages you would need to add some lines of code and recompile everything just to know the type declared.

So, other than to know the return type of a function which clearly isn't a strength point of statically typed languages, how is static typing really helpful in bigger projects?

16
  • 6
  • 2
    if you read the answers to the other question, you will probably get the answers you need for this one, they are basically asking the same thing from different perspectives :)
    – sara
    Jul 16, 2016 at 10:10
  • 1
    Swift and playgrounds are a REPL of a statically typed language.
    – daven11
    Jul 16, 2016 at 11:14
  • 2
    Languages aren't compiled, implementations are. The way to write a REPL for a "compiled" language is to write something that can interpret the language, or at least compile and execute it line by line, keeping the necessary state around. Also, Java 9 will ship with a REPL. Jul 16, 2016 at 20:39
  • 2
    @user6245072: Here's how to make a REPL for an interpreter: read code, send it to the interpreter, print out result. Here's how to make a REPL for a compiler: read code, send it to the compiler, run the compiled code, print out result. Easy as pie. That's exactly what FSi (the F♯ REPL), GHCi (GHC Haskell's REPL), the Scala REPL, and Cling do. Jul 18, 2016 at 15:57

6 Answers 6

23

More over, with REPLs it's trivial to test a function for it's return type with different inputs

It's not trivial. It's not trivial at all. It's only trivial to do this for trivial functions.

For instance, you could trivially define a function where the return type depends entirely on the input type.

getAnswer(v) {
 return v.answer
}

In this case, getAnswer doesn't really have a single return type. There is no test you can ever write that calls this with a sample input to learn what the return type is. It will always depend on the actual argument. At runtime.

And this doesn't even include functions that, e.g., perform database lookups. Or do things based on user input. Or look up global variables, which are of course of a dynamic type. Or change their return type in random cases. Not to mention the need to test every single individual function manually every single time.

getAnswer(x, y) {
   if (x + y.answer == 13)
       return 1;
   return "1";
}

Fundamentally, proving the return type of the function in the general case is literally mathematically impossible (Halting Problem). The only way to guarantee the return type is to restrict the input so that answering this question does not fall under the domain of the Halting Problem by disallowing programs that are not provable, and this is what static typing does.

As an addition, as functions are declared with type funcname()..., whitout knowing type you will have to search over each line in which the function is called, because you only know funcname, while in Python and the like you coud just search for def funcname or function funcname which only happens once, at the declaration.

Statically typed languages have things called "tools". They are programs that help you do things with your source code. In this case, I would simply right click and Go To Definition, thanks to Resharper. Or use the keyboard shortcut. Or just mouse over and it will tell me what the types involved are. I don't care in the slightest about grepping files. A text editor on its own is a pathetic tool for editing program source code.

From memory, def funcname would not be enough in Python, as the function could be re-assigned arbitrarily. Or could be declared repeatedly in multiple modules. Or in classes. Etc.

and you will still have to return to the line in which it's declared using the search function of your text editor to check it.

Searching files for the function name is a terrible primitive operation that should never be required. This represents a fundamental failure of your environment and tooling. The fact that you would even consider needing a text search in Python is a massive point against Python.

2
  • 3
    To be fair, those "tools" were invented in dynamic languages, and dynamic languages had them long before static languages did. Go To Definition, Code Completion, Automated Refactoring etc. existed in graphical Lisp and Smalltalk IDEs before static languages even had graphics or IDEs, let alone graphical IDEs. Jul 18, 2016 at 0:03
  • Knowing the return type of functions doesn't always tell you what functions DO. Instead of writing types you could have written doc tests with sample values. for example, compare (words 'some words oue') => ['some', 'words', 'oeu'] with (words string) -> [string], (zip {a b c} [1..3]) => [(a, 1), (b, 2), (c, 3)] with its type.
    – aoeu256
    Sep 22, 2019 at 18:37
20

Think of a project with many programmers, that has changed over the years. You have to maintain this. There's a function

getAnswer(v) {
 return v.answer
}

What on earth does it do? What's v? Where does the element answer come from?

getAnswer(v : AnswerBot) {
  return v.answer
}

Now we have some more info —; it needs a type of AnswerBot.

If we go to a class-based language we can say

class AnswerBot {
  var answer : String
  func getAnswer() -> String {
    return answer
  }
}

Now we can have a variable of type AnswerBot and call the method getAnswer and everyone knows what it does. Any changes are caught by the compiler before any runtime testing is done. There are many other examples but perhaps this gives you the idea?

13
  • 1
    Looks already clearer - unless you point out that a function like that has no reason to exist, bu that's of course just an example. Jul 16, 2016 at 9:15
  • Thats the trouble when you have multiple programmers on a large project, functions like that do exist (and worse), it's the stuff of nightmares. also consider functions in dynamic languages are in the global namespace, so over time you could have a couple of getAnswer functions - and they both work and they're both different because they're loaded at different times.
    – daven11
    Jul 16, 2016 at 9:19
  • 1
    I guess it's a misunderstanding of functional programming that causes that. However, what do you mean by saying they're in the global namespace? Jul 16, 2016 at 9:22
  • 3
    "functions in dynamic languages are by default in the global namespace" this is a language specific detail, and not a constraint caused by having dynamic typing.
    – sara
    Jul 16, 2016 at 10:15
  • 2
    @daven11 "I'm thinking javascript here", indeed, but other dynamic languages have actual namespaces/modules/packages and can warn you on redefinitions. You might be over-generalizing a bit.
    – coredump
    Jul 16, 2016 at 18:37
10

You seem to have a few misconceptions about working with large static projects that may be clouding your judgement. Here are some pointers:

even if you declare the return type of a function, you can and will forget it after you've written many lines of code, and you will still have to return to the line in which it's declared using the search function of your text editor to check it.

Most people working with statically typed languages use either an IDE for the language or an intelligent editor (such as vim or emacs) that has integration with language-specific tools. There is usually a fast way of finding the type of a function in these such tools. For example, with Eclipse on a Java project, there are two ways you'd typically find the type of a method:

  • If I want to use a method on another object than 'this', I type a reference and a dot (e.g. someVariable.) and Eclipse looks up the type of someVariable and provides a drop down list of all the methods defined in that type; as I scroll down the list the type and documentation of each is displayed while it is selected. Note that this is very difficult to achieve with a dynamic language, because it is hard (or in some cases impossible) for the editor to determine what the type of someVariable is, so it cannot generate the correct list easily. If I want to use a method on this I can just press ctrl+space to get the same list (although in this case it is not that hard to achieve for dynamic languages).
  • If I already have a reference written to a specific method, I can move the mouse cursor over it and the type and documentation for the method is displayed in a tooltip.

As you can see, this is somewhat better than the typical tooling available for dynamic languages (not that this is impossible in dynamic languages, as some have pretty good IDE functionality -- smalltalk is one that jumps to mind -- but it is harder for a dynamic language and therefore less likely to be available).

As an addition, as functions are declared with type funcname()..., whitout knowing type you will have to search over each line in which the function is called, because you only know funcname, while in Python and the like you coud just search for def funcname or function funcname which only happens once, at the declaration.

Static language tools typically provide semantic search capabilities, i.e. they can find definition of and references to particular symbols precisely, without needing to perform a text search. For example, using Eclipse for a Java project, I can highlight a symbol in the text editor and right click it and choose either 'go to definition' or 'find references' to perform either of these operations. You don't need to search for the text of a function definition, because your editor already knows exactly where it is.

However, the converse is that searching for a method definition by text really doesn't work as well in a large dynamic project as you suggest, as there could easily be multiple methods of the same name in such a project, and you likely have no readily available tools to disambiguate which one of them you are invoking (because such tools are hard to write at best, or impossible in the general case), so you will have to do it by hand.

More over, with REPLs it's trivial to test a function for it's return type with different inputs

It isn't impossible to have a REPL for a statically typed language. Haskell is the example that springs to mind, but there are REPLs for other statically typed languages too. But the point is that you don't need to execute code to find the return type of a function in a static language -- it can be determined by examination without needing to run anything.

while with staticly typed languages you would need to add some lines of code and recompile everything just to know the type declared.

Chances are even if you did need to do this, you wouldn't have to recompile everything. Most modern static languages have incremental compilers that will only compile the small portion of your code that has changed, so that you can get almost instantaneous feedback for type errors if you make one. Eclipse/Java, for example, will highlight type errors while you're still typing them.

2
  • 4
    You seem to have a few misconceptions about working with large static projects that may be clouding your judgement. Well, I'm only 14 years old and I only program from less than an year on Android, so it's possible I guess. Jul 17, 2016 at 12:46
  • 1
    Even without an IDE, if you remove a method from a class in Java and there are things that depend on that method, any Java compiler will give you a list of every line that was using that method. In Python, it fails when the executing code calls the missing method. I use both Java and Python regularly and I love Python for how quickly you can get things running and the cool things you can do that Java doesn't support but the reality is that I have problems in Python programs that just don't happen with (straight) Java. Refactoring in particular is much more difficult in Python.
    – JimmyJames
    Jul 18, 2016 at 21:10
6
  1. Because static checkers are easier for statically typed languages.
    • At a bare minimum, with no dynamic language features, if it compiles, then at runtime there are no unresolved functions. This is common in ADA projects and C on microcontrollers. (Microcontroller programs get big sometimes... like hundreds of kloc big.)
  2. Static compile reference checks are a subset of function invariants, which in a static language can also be checked at compile-time.
  3. Static languages usually have more referential transparency. The result is that a new developer can dive in to single file and understand some of what's going on, and fix a bug or add a small feature without having to know all the strange things in the codebase.

Compare with say, javascript, Ruby or Smalltalk, where developers do redefine core language functionality at run time. This makes understanding the large projct harder.

Bigger projects don't just have more people, they have more time. Enough time for everyone to forget, or move on.

Anecdotally, an acquaintance of mine has a secure "Job For Life" programming in Lisp. Nobody except the team can understand the code-base.

5
  • Anecdotally, an acquaintance of mine has a secure "Job For Life" programming in Lisp. Nobody except the team can understand the code-base. is it really that bad? Doesn't the personalization they added help them being more productive? Jul 17, 2016 at 7:09
  • @user6245072 It may be an advantage for the people currently working there, but it makes recruiting new people harder. It takes more time to find someone who already knows a non-mainstream language or to teach them one they don't know already. This can make it harder for the project to scale up when it gets successful, or to recover from fluctuation - people do move away, get promoted to other positions... After a while, it can also be a disadvantage for the specialists themselves - once you've only written some nich-language for a decade or so, it may be hard to move on to something new.
    – Hulk
    Jul 18, 2016 at 9:55
  • Can't you just use a tracer to create unit tests from the running Lisp program? Like in Python you can create a decorator(adverb) called print_args that takes in a function and returns a modified function that prints out its argument. You can then apply it to the whole program in sys.modules, although an easier way to do it is to use sys.set_trace.
    – aoeu256
    Sep 22, 2019 at 18:54
  • @aoeu256 I'm not familiar with Lisp runtime environment capabilities. But they did use macros heavily, so no normal lisp programmer could read the code; It's likely that trying to do "simple" things to the runtime can't work due to the macros changing everything about Lisp. Sep 23, 2019 at 5:11
  • @TimWilliscroft You could wait until all of the macros are expanded before doing that kind of stuff. Emacs has a lot of shortcut keys to let you inline expand macros (and inline functions maybe).
    – aoeu256
    Sep 28, 2019 at 23:12
4

I never understood statements like this one. To be honest, even if you declare the return type of a function, you can and will forget it after you've written many lines of code, and you will still have to return to the line in which it's declared using the search function of your text editor to check it.

It's not about you forgetting the return type -- this is always going to happen. It's about the tool being able to let you know that you forgot the return type.

As an addition, as functions are declared with type funcname()..., whitout knowing type you will have to search over each line in which the function is called, because you only know funcname, while in Python and the like you could just search for def funcname or function funcname which only happens once, at the declaration.

This is a matter of syntax, which is completely unrelated from static typing.

The C family syntax is indeed unfriendly when you want to look up a declaration without having specialized tools at your disposal. Other languages don't have this problem. See Rust's declaration syntax:

fn funcname(a: i32) -> i32

More over, with REPLs it's trivial to test a function for it's return type with different inputs, while with statically typed languages you would need to add some lines of code and recompile everything just to know the type declared.

Any language can be interpreted and any language can have a REPL.


So, other than to know the return type of a function which clearly isn't a strong point of statically typed languages, how is static typing really helpful in bigger projects?

I'll answer in an abstract way.

A program consists of various operations and those operations are laid out the way they are because of some assumptions the developer makes.

Some assumptions are implicit and some are explicit. Some assumptions concern an operation near them, some concern an operation away from them. An assumption is easier to identify when it is expressed explicitly and as close as possible to the places where its truth value matters.

A bug is the manifestation of an assumption that exists in the program but doesn't hold for some cases. To track down a bug, we need to identify the erroneous assumption. To remove the bug, we need to either remove that assumption from the program or change something so that the assumption actually holds.

I'd like to categorize assumptions into two kinds.

The first kind are the assumptions that may or may not hold, depending on the inputs of the program. To identify an erroneous assumption of this kind, we need to search in the space of all possible inputs of the program. Using educated guesses and rational thinking, we can narrow down the problem and search in a much smaller space. But still, as a program grows even a little bit, its initial input space grows at an enormous rate -- to the point where it can be considered infinite for all practical purposes.

The second kind are the assumptions that definitely hold for all inputs, or are definitely erroneous for all inputs. When we identify an assumption of this kind as erroneous, we don't even need to run the program or test any input. When we identify an assumption of this kind as correct, we have one less suspect to care about when we're tracking down a bug (any bug). Therefore, there is value in having as many assumptions as possible belong to this kind.

To put an assumption in the second category (always true or always false, independent of inputs), we need a minimum amount of information to be available at the place where the assumption is made. Across a program's source code, information gets stale pretty quickly (for example, many compilers don't do interprocedural analysis, which makes any call a hard boundary for most information). We need a way to keep the required information fresh (valid and nearby).

One way is to have the source of this information as close as possible to the place where it's going to be consumed, but that can be impractical for most use cases. Another way is to repeat the information frequently, renewing its relevance across the source code.

As you can already guess, static types are exactly that -- beacons of type information scattered across the source code. That information can be used to put most assumptions about type correctness in the second category, meaning that almost any operation can be classified as always correct or always incorrect with respect to type compatibility.

When our types are incorrect, the analysis saves us time by bringing the bug to our attention early rather than late. When our types are correct, the analysis saves us time by ensuring that when a bug occurs, we can immediately rule out type errors.

3

You remember the old adage "garbage in, garbage out", well, this is what static typing helps to prevent. Its not a universal panacea but the strictness over what kind of data a routine accepts and returns means that you have some assurance that you're working correctly with it.

So a getAnswer routine that returns an integer will not be useful when you try to use it in a string-based call. The static typing s already telling you to watch out, that you're probably making a mistake. (and sure, you can then override it, but you'd have to know exactly that's what you're doing, and specifying it in the code using a cast. Generally though, you do not want to be doing this - hacking in a round peg into a square hole never works well in the end)

Now you can take it further by using complex types, by creating a class that has ore functionality, you can start passing these around and you suddenly get much more structure in your program. Structured programs are ones that are much easier to make work correctly, and also maintain.

1
  • You don't have to do static type inference (pylint), you can do dynamic type inference chrislaffra.blogspot.com/2016/12/… which is also done by PyPy's JIT compiler. There is also another version of dynamic type inference where a computer randomly places mock objects in the arguments and sees what causes an error. The halting problem doesn't matter for 99% of cases, if you take too much time just stop the algorithm (this is how Python handles infinite recursion, it has a recursion limit that can be set).
    – aoeu256
    Sep 22, 2019 at 19:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.