Hacker News new | past | comments | ask | show | jobs | submit login
Types are a basic tool of software design (2018) (tedinski.com)
103 points by gus_leonel 11 days ago | hide | past | favorite | 118 comments





I go a little back and forth on this with my experience in F#, which relies heavily on inferred types. You can write a lot of F# before you need to add type annotations, but eventually, things become a spiderweb. The key issue is when you make a 'small' change to some method/value, the changes ripple through the program creating confusing errors sometimes where the compiler is trying to knit things together.

After a while, I found myself adding back types in a decent number of places to "anchor" the type inference, indicating that a certain type/signature is fixed and a change should be carefully considered.

I still don't know how folks deal with these kinds of changes in weakly typed languages without always allowing bugs to pour into their code over time. But I do love the "move fast" and low boiler-plate aspects of "typeless" coding.


I think what a lot of people miss is that this ripple effect always exists. Whether you have strong types or weak types, it's hard and often impossible to avoid.

All strong types do is make it explicit at compile time (and with static analysis), instead of leaving it to be discovered in tests or at runtime.

This is why I'll use Python for small one-off scripts but anything that will be in regular use I prefer to do in Java. Even though I have many more years of experience with Python.


As someone who works in C# and TypeScript, I suspect that people who work in weakly typed languages either:

- Don’t have big projects.

- Have lots of unit tests to cover some of what a C# or Java compiler would have caught through static analysis.

- Don’t even consider doing certain kinds of refactoring (which would be trivial on a strongly typed language) because they have no way of knowing what will break.


4th option, they are just better than you are at managing dynamism.

Even ignoring for a moment that I consider being highly practiced at dynamism a largely pointless skill while strongly typed languages exist, sooner or later a code base which is always growing will reach a certain size where even the very best person in the world at dynamism will have to resort to unit tests to cover what a compiler can do for free, or just accept that certain refactoring or changes to the code base are unreasonably expensive to do with any reasonable level of confidence.

I'm the guy with a huge javascript codebase, written before typescript existed, and I have none of the problems you describe. Huge refactors are also not a necessity in every codebase, if the code was written well to begin with. And so far using typescript in other projects has not produced the supposed benefits a lot of people say are inherent with typescript. There is no magic happening that saves me from writing bad code, because I wasn't writing bad code before typescript. Refactoring isn't all that difficult either, even without tests. But I guess YMMV.

Good code becomes bad when the requirements change sufficiently in ways the original design didn't anticipate.

Great, but that doesn't describe every Javascript project or use of Javascript. If you're describing big changes, chances are a refactor isn't what's needed, a rewrite is. And even when requirements change, it doesn't mean Javascript can't be refactored. It depends on the skill of the team. If you want to hire idiots then you're going to need more than strong types to get anything shipped. I doubt types would really help that much in some places because programmers love to invent their own footguns.

The fact you think that big changes more often than not necessitate a rewrite makes you sound like someone who’s never worked on a very large strongly typed code base where big changes and refactors are absolutely possible, and happen in a reasonable timeframe, without having to resort to a rewrite.

Of course it’s not impossible to have a good JavaScript code base, it’s just much harder to have a very large one where it’s still economically feasible to make significant changes to it without having to resort to a rewrite or needing to write tests which would be covered by the compiler in a strongly typed language.


>makes you sound like

Too bad you don't know me so you're left to ad-hominem attacks on my expertise.

>without having to resort to a rewrite.

Glad you get to move the goalposts anywhere you want to justify any comment you make. You haven't worked on every codebase that ever existed, even though it sounds like you think you have.

In many cases, yes, it is worth a rewrite instead of trying to shoehorn something that exists, only because it exists but is the wrong solution for the new requirements. We're not going to get into the weeds of every kind of refactor in every kind of codebase here on HN, so instead you can move the goalposts and I'll just stop replying here. We can agree to disagree and call it a day.


> In many cases, yes, it is worth a rewrite instead of trying to shoehorn something that exists

I also haven't seen every codebase in existence, but I have seen enough "it's only going to take a year" rewrites that absolutely should have been refactorings and so I remain rather skeptical of the claim that rewrites make sense in "many cases".


Thanks for your anecdotal comment, but it really only means something to you.

I could say the same about your comments. Your snarkiness is not appropriate for this website. Don't start commenting if you're not open to having your views challenged.

> Huge refactors are also not a necessity in every codebase, if the code was written well to begin with.

If dynamically typed languages only worked well in absolutely pristine codebases that never saw any hacks or poorly thought out solutions, I'm not sure they'd be actually useful in most real-life settings. Of course, people refactor all the time in dynamically typed languages as well.


They probably are also more beautiful too huh?

Is it really too much to ask to use the correct terms here, static and dynamic typing?

Strong and weak are sort of coherent as a spectrum, but they aren't a typology, and they do not in any sense or in any case reduce to static vs. dynamic types. Conflating them is not useful: I can make a good case that Julia's type system is stronger than C's, but Julia is dynamically typed and C is statically typed.

There's no reason to keep doing this. It's a malapropism, we could just.. not say that. Especially in a thread which is specifically about types.


Sorry, you're right of course. I was on the phone and in a hurry and just took the terminology of the comment I was responding to without thinking about it. Static and dynamic typing are the correct terms. Strong/weak typing is the difference between Python and C++, which are (inversely) dynamically and statically typed respectively.

I agree with this sentiment but view it inherently as a feature of languages like f# and Haskell - That small ripple at compile times makes trusting refractors easier. After having experienced this, languages like python become really challenging to grok without heavy unit testing.

There's also a middle ground which is to support function-local type inference, but not inter-function type inference, which guarantees that such an "anchor" is never all that far away (especially never in another file). This is the approach Rust uses.

> You can write a lot of F# before you need to add type annotations, but eventually, things become a spiderweb. The key issue is when you make a 'small' change to some method/value, the changes ripple through the program creating confusing errors sometimes where the compiler is trying to knit things together.

I found working with type inference being its own skill. If you know how to place type annotations well, the ripple effect only affects one or two callsites and then the type inference just continues to infer what you meant in the subsequent code. Though we may be approaching the structuring of the code differently so YMMV. But I haven't had issues even with complicated member constraints which replicate dependent typing.


Having worked on a large js codebase back before typescript or flow or the closure compiler existed, I found that the process was not very different to any code - you check your preconditions on entry to code that will be called from elsewhere, it's just that those preconditions in a dynamically typed language may include the types of your arguments. If you do that, then type errors typically cause your code to fail fast, often on first load, in easy to understand ways. Overhead is just a couple of extra easy to understand lines at the top of about half your functions (probably less boilerplate than go's error handling forces on you).

Given the fantastic iteration speed that the Web platform had (and still does to some extent) I didn't really miss a compiler for catching errors. The main improvement is that IDEs find it easier to support big refactoring.

I also worked on a mid sized scala codebase a little later on. Scala is probably better now than it was then, but despite (perhaps because of?) the cleverer type system, everything was so slow that it actually took longer for many bugs to be highlighted by the compiler than they would have been found by hitting f5 in a browser window with a good js codebase. That was when I realised that as a developer I care a lot about when an error is highlighted in wall clock time and not at all about which compiler phase it was discovered in.


I think Scala has a good compromise here, where all function signatures require explicit types and (almost) everything else can be inferred. Maybe F# does the same? I find that this is enough "anchoring" to allow for sensible error messages most of the time, although occasionally I'll sprinkle in more annotations if the inferred types would be particularly confusing to readers (usually because they're too general).

> the changes ripple through the program creating confusing errors sometimes where the compiler is trying to knit things together.

F# is derived from OCaml, and they both support interface files–in the case of F#, with the .fsi extension–which is designed to solve exactly this problem. After settling on the surface area of your module, you nail it down by writing an interface file with the exact types you want to enforce. The compiler then uses this to check the usage of the module by its consumers, and provides much more accurate type errors.

It works really well.


Glad F# was mentioned, my mind went their immediately as well. The ripple is definitely intentional, and from what I've found the dotnet compiler is quite performant so not so much experiencing the feedback lag that other mentioned when it comes to Scala's compile time. The thing I appreciate of F# is that if it compiles it probably "just works".

I love this before/after example of TypeScript being removed from a library as a clear illustration that only occasional simple type annotations are needed: https://github.com/hotwired/turbo/pull/971/files

Looks like so much useful documentation for understanding the code, assistance for refactoring, and static checking (which are like always running, fast and exhaustive unit tests for free) for a modest amount of type annotations that each person on the team would need to recreate in their heads anyway. Static type checking is so useful for catching runtime errors to do with optional values and index/key not found error too, that always create a load of edge cases.

Editing JavaScript with no types is scary, especially when you didn't write it. You're mostly guessing what the types must be and hoping your running code exercises enough edge cases to catch any problems. "You should write more tests" as an alternative rings hollow for me because (for the things they will check for you) types are more concise, exhaustively check all values, are ran continuously, are fast, aid with automatic refactoring, document the code in the same file its in, and will locate exact snippets that are causing errors. More often than not, test mostly check happy paths, aren't even close to as exhaustive, and nobody is writing tests that check every combination of function parameters being null vs non-null, string vs number etc. (which would be a huge amount of noise in your test suite that would slow down refactoring too). Types are a compliment to tests as well, not an alternative.

I can't relate to arguments about types getting in the way (as long as you avoid trying to get too clever like with complex generics and TypeScript conditional types). Usually when this happens, it's because it's hard to reason the runtime edge cases in your own head and there's probably a simpler way to write it. I'd love a good non-niche example to the contrary. Worst case in TypeScript, you can use `any` as an escape hatch anyway so as long as you can add types to most of your program it's not a strong reason against for me.


> This one is interesting. I would have no idea what a "submitter's" purpose is here with or without Typescript.

> I am excited to see how this code base changes without Typescript. The "types" or what things are will have to be communicated in some way. This may lead to very readable code.

This is funny to me. "If we don't have the compiler checking types the developers will perfectly compensate with documentation, an improvement." Maybe remove the unit tests as well, then the developers will write accurate code.


It's using that expendable developer time to free some precious extra developer-machine-CPU time.

Who wouldn't want to spend some weeks or hard labor so their computers can work for a few less seconds? Let's keep what's really important in mind!


That PR is great. Not only do they get rid of TypeScript, they get rid of prettier and some gratuitous trailing commas. https://github.com/hotwired/turbo/pull/971/files#diff-865b8f...

I’m not joking. I prefer the after.

Edit: thanks. Yes, commas, not comments.


Funny, I have exactly the opposite feelings.

I've worked on dynamically typed code-bases in the past, and also on code-bases without formatters like prettier... and I'm not going back there.

So many odd bugs can be avoided simply by taking a little bit of time and declaring some types. It doesn't have to be a super fancy type-system (I actually prefer if it isn't), but I really want to know whether something is expected to be a string or a number, or what properties an object can be expected to have, or what values a literal can accept, and so on. It's just so frustratingly time-consuming to figure out in hindsight. And all for some person's odd perception of simplicity.

Same for formatters - all the squabbling over some personal preferences on how code should be formatted. Hell no. Let's install some opinionated formatter and let it do its job, I want to tackle more interesting problems than yet another narrow-minded discussion about necessity of semicolons, placement of whitespace or line-length.

I'm at a point where I straight-up refuse to work with people who prefer to work that way; or at least require them to be a few departments away from me. They can be programming gods; if they can't be arsed to type their code so I can immediately grok it, they can program elsewhere.


Same.

I sometimes write TypeScript for 2 hours straight without actually running the code or any unit test, and it would just work -- the return value is exactly what you want. I don't think I can do that for JavaScript for more than 15 minutes even though they are the "same language".


I could not disagree more, but I am obviously open to it being a personal preference. I've spent months updating JavaScript codebases that are between 3 and 5 years old, most of them written with vanilla JavaScript. A _few_ of them were written with TypeScript at least partially and those files were the least mentally taxing to upgrade.

Some of the codebases had tests, a good chunk of which could (and were) eliminated simply by using TSC as part of the build process. I think allowing the compiler and tooling like prettier to make choices for you leaves more space for you to think about the problem, not the myriad of problems _around_ it.


> I’m not joking. I prefer the after.

Everyone likes the simplicity that comes with removing guardrails, but when easily preventable problems happen that's glanced over as an unavoidable fact of life.


> but when easily preventable problems happen

easily preventable is doing a lot of work here. This is assuming the types are already written and never need to be maintained. How often does the type checker catch a problem with the code vs the type definitions themselves?


> How often does the type checker catch a problem with the code vs the type definitions themselves?

All the time? I've worked on code bases with multiple people without types, and whenever there's a place that can produce e.g. "string or undefined" like when a key might not be found in a collection, it produces edge cases everywhere that don't get noticed outside of the happy path, including after the code has been merged. You're forced to simulate in your head all the code paths, and remember which values might be optional which is error-prone, isn't scalable and eats up brain cycles.

Even typos like `object.detail.name` vs `object.details.name` cause runtime errors, or calling a function with the wrong number of arguments or in the wrong order, some of the most trivial bugs you can think of.

I don't understand how someone can dislike writing type annotations so much that they're happy having their time wasted like this when automated assistance is right there. To me, it's like not wanting to use a spellchecker when writing an article, or opting out of syntax checking code before it's ran.

I get that problems with type definitions can be frustrating but it's better than runtime errors (having to write more tests isn't better either), and often flags actual bugs, potential future bugs ("what if this was undefined sometimes?"), or overly dynamic code that's hard to reason about (e.g. conditionally adding/removing fields from objects, or having a field that changes type between string/array/undefined vs using an array of strings instead). I'd love a counter example here - often the examples I see has overly dynamic code because the coder hasn't had a strong incentive to avoid such code before. As in, I write type checker friendly code so I can benefit from type checking.

90% of type annotation stuff I work with is mundane and simple (e.g. strings, numbers, arrays, something optional, object with basic fields) and is well worth the minimal effort. The more complex stuff isn't needed often, usually optional, and gives you protection from tricky bugs too.


> Even typos like `object.detail.name` vs `object.details.name` cause runtime errors, or calling a function with the wrong number of arguments or in the wrong order, some of the most trivial bugs you can think of.

In Clojure, a dynamically typed language, referencing a var that is not declared, or calling a function with the wrong number of arguments will result in a compilation error. You don't need static typing or type annotations for this.

> I don't understand how someone can dislike writing type annotations so much that they're happy having their time wasted like this when automated assistance is right there. To me, it's like not wanting to use a spellchecker when writing an article, or opting out of syntax checking code before it's ran.

It's not about liking or disliking, it's about "wasting time." Spell checking and syntax checking are free, annotating everything with types and maintaining them is not.


> In Clojure, a dynamically typed language, referencing a var that is not declared, or calling a function with the wrong number of arguments will result in a compilation error. You don't need static typing or type annotations for this.

Can it catch problems like e.g. if you try to access a `y` field on some input object but only field `x` is defined? And if you want to define a function that takes a callback that must accept two arguments? What about ways to force you to check a variable with type `string | undefined` is defined before you do something with it? At some point it must breakdown because more complex checks can't be inferred?

> it's about "wasting time." ... annotating everything with types and maintaining them is not.

See this example. I'd argue the type annotations are minimal, but you gain a lot of safety, documentation and refactoring help from them: https://github.com/hotwired/turbo/pull/971/files


The arity check in clojure sounds like a nice balance between runtime and compile time errors.

Having runtime errors is the point - that's real dynamic typing for you. If not for the possibility of runtime errors, you'd have to do more to have the code run. A lot of junior devs who prefer static typing don't get it. Senior devs who prefer static typing at least understand that some have a different preference. Of course you don't want a lot of runtime errors to make it to production, but in development, yes.

There are those who think that the IDE should provide feedback at every keystroke. Most IDEs have this opinion built into them. But I don't like it very much. It's gotten to the point where there isn't an IDE I really like. I miss Atom.


> To me, it's like not wanting to use a spellchecker when writing an article

Huh, I'm absolutely in the pro-static-types camp, but I never use spellcheck which I personally find distracting and unhelpful. I wouldn't consider those two be similar issues at all.


> I'm absolutely in the pro-static-types camp

Reflecting on your comments on the Transcendental Syntax threads, you should really look into linear logic, or at least System F (https://en.wikipedia.org/wiki/System_F).


Definitely will at some point

me too!

> easily preventable is doing a lot of work here.

It isn't. It's the most basic value proposition of TypeScript.

> This is assuming the types are already written and never need to be maintained.

No, it doesn't.

You need to write software so that you get an application that does what you wish it to do. Does this surprise you? This is not something that was invented by TypeScript.

When a software developer wants to prevent bugs when adding features, what they do is add preconditions and post-conditions to enforce invariants, and they handle code paths specific to both the happy path and failure modes. What static typing does is ensure these checks can be done at compile time, and that failure modes can be prevented by ensuring data types that do not meet conditions cannot be passed to functions.

TypeScript allows developers to check preconditions and post-conditions by defining types and adding type assertions that verifies whether an object is of a certain type, and then assert at compile-time if they meet preconditions and post-conditions. If they specify a type that has specific characteristics, the typescript compiler helps them by automatically checking preconditions and post-conditions, and prevent code paths that are proven to be failure-prone.

This is the very basic of software development. This can only be interpreted as "maintenance" or "extra work" by someone who thinks it's ok if the program crashes and malfunctions if preventable failure modes are not prevented. This is not a trait of a programming language. This is a trait of your and the level of quality at which you do your job.


What's the benefit of removing the explicit typing? It strikes me as hamstringing readability of the code. This just feels like giving up and calling the status quo "beautiful" or something.

To reinvent it.

People tried to argue that in the PR, suggesting to change variable names.

This one: https://github.com/hotwired/turbo/pull/971/files#r1317386731

> function buildFormData(formElement: HTMLFormElement, submitter?: HTMLElement): FormData {

> It might be useful to know that submitter is optional argument here.

> How would one express that? The variable now needs to be renamed?

And two lines later:

> const name = submitter?.getAttribute("name")

It's already easy to see that submitter is optional.


> It's already easy to see that submitter is optional.

Why force yourself to read inside function implementations like this for every function you might use when fool-proof automated assistance is right there? What if the function was tens of lines long? What if the optional value only appeared inside a chain of 5 nested functions? How would you easily fix/update your code if the value isn't optional now but got changed to be optional in the future?

Manual effort like this is error-prone, not scalable, and a huge distraction. Why would avoiding type annotations be worth this?


Because this is an internal function in a _library_, and you should be willing to look inside it if you're changing a call to it.

Here's where whether or not it's defined can be traced to: https://github.com/hotwired/turbo/blob/41c074ff113a8882aadbc...

Maybe the types should have been written differently, to indicate that it's the type of the submitter property in a FormEvent. That's another cost of using TypeScript, is trying to get the types right. TMTOWTDI.


> Because this is an internal function in a _library_

Why does that matter? Error-prone approaches create buggy code and we don't want buggy code, whether it's a library or something else. Libraries can be huge and complex, and arguably should have more robust edge case checking than regular code.


Only easy to see that it's optional if you read the source code vs. looking at the auto definition in your IDE/in autogenerated documentation.

You're only able to see it by reading the function body.

I can't imagine caring that much about trailing commas. The point of a tool like prettier is to not sweat such irrelevant details.

Trailing commas actually have an important role.

Let’s say you have some code :

  const foo = {
    x: 1,
    y: 2,
  };
and you want to add a field :

  const foo = {
    x: 1,
    y: 2,
    z: 3,
  };
Without trailing commas, the diff is

  - y: 2
  + y: 2,
  + z: 3
With trailing commas :

  + z: 3,
The second diff is actually much clearer at what’s happening.

This is why it irritates me when people forget the trailing comma. It’s not a problem in the moment. But I know it will be a small but avoidable cognitive burden on the next PR (wait, why did "y" changed ? Oh, right, it didn’t).


Maybe diff should change to suit programs, rather than programs change to suit diff.

You want diff to know the syntax and semantics of every programming language out there ?

This is possible already with tools like difftastic.

https://github.com/Wilfred/difftastic


Difftastic would not solve the issue described by madeofpalk because it still highlights the added comma. You need a diff tool that can distinguish between optional and required syntax. So far I am not aware of any tool that supports this, except the one I am working on (SemanticDiff).

I'm pretty sure this counts. https://github.com/afnanenayet/diffsitter

Certainly the idea has been suggested many times. I think people end up formatting both before/after and doing a diff on formatted before against formatted after. I've done that.


It would be like a syntax highlighter. It’s doable.

But diff does not exists in a vacuum. It would need to be integrated to IDEs, editor, merge tools, PR tools. You now have to have `patch` understand and depend on the details of the syntax of your language. In a way that may break between different versions of diff and patch. Not even starting with variants of a language, different interpretations of how to handle the preprocessor/macros system, different editions of the same language.

All that, just to not have to add a trailing comma ?


Most things like patch would still use line diff. I think I would want this semantic diff in the merge request review and the logs. It could have definitions distributed with the syntax highlighters in language plugins in an editor/IDE, git CLI, and git forge.

So GitHub, git lab, gutbucket, gittea (and its 50 forks), fossil, and all the other tools should change, not the formatting of the code?

No, having a concise separator is still preferable to saving a line in a diff.

Indeed. That’s why prettier ain’t pretty.

I also like it because I can reorder items in an array without having to also add/remove a comma!

Sure, I can get behind that. That's why we just automate these trivial things away.

Even with the find/replace errors that seemingly got merged? This PR feels like it was so rushed

s/ent/a

That PR reminds me why I'm glad that I left the Rails world, despite multiple HN threads recently touting its benefits. Ultimately, DHH just decides things on a whim and ignores everyone's objections.

I really like the balance struck by Ada's type system, especially Application-Defined types to model the problem domain - https://learn.adacore.com/courses/Ada_For_The_CPP_Java_Devel...

Other effective features of Ada's type system:

- type ranges (https://learn.adacore.com/courses/Ada_For_The_CPP_Java_Devel...)

- generalized type contracts using subtype predicates (https://learn.adacore.com/courses/Ada_For_The_CPP_Java_Devel...)

- attributes (https://learn.adacore.com/courses/Ada_For_The_CPP_Java_Devel...)


Since adopting typscript, the way I usual approach my development is that I clearly define data types on ui and then the implementation details tend to reveal themselves.

There is a quote I saw somewhere, forget where, but it was basically: define your data structure and the algorithms will reveal themselves.


> Since adopting typscript, the way I usual approach my development is that I clearly define data types on ui and then the implementation details tend to reveal themselves.

That is aligned with the basic premise of Domain-Driven design: define a model of the problem domain, and everything else just falls in place.

I wonder why these articles reinvent the wheel with functional programming hand-waving when DDD has been screaming this for years.


> I wonder why these articles reinvent the wheel with functional programming hand-waving when DDD has been screaming this for years.

These are two different levels of abstraction. If you're still enraptured by the fundamental mechanisms of computation (i.e. functional programming) you're not going to see the value in trying to figure out the best way to build via composing mechanisms of computation, whether it's functional, imperative, procedural, logic-based, lisp-based, object-oriented, message-passing, whatever. There's a reason why there aren't many DDD frameworks out there—it doesn't really benefit from tying to specific technologies/languages/ecosystems in the same way that the culture of "build shit as fast as possible" attitude dovetails well with the goals of Rails.

I don't see why this is surprising; people re-discover old lessons daily, and petty details will always be conflated with broader themes and patterns—it's just how the human brain works. The discussion is what keeps us thinking about those old lessons. This is just a person's blog, not a journal trying to represent an industry.


> There's a reason why there aren't many DDD frameworks out there—it doesn't really benefit from tying to specific technologies/languages/ecosystems in the same way that the culture of "build shit as fast as possible" attitude dovetails well with the goals of Rails.

I think your comment fits the "not even wrong" category. It goes beyond nonsense.

There is no DDD framework because the very idea of a DDD framework makes absolutely no sense at all. DDD is an approach to software design, not something you build and reuse.

DDD is then used to specify what types to define to put together a model of the domain. Again, there is absolutely no point to have a framework to define these.

Lastly, DDD defines interfaces for domain services, and imposed absolutely no restrictions on how to define those. This has nothing to do with frameworks.

Development speed is also irrelevant and unrealistic.

Not even wrong.


Could it be Linus Torvalds' "Bad programmers worry about the code. Good programmers worry about data structures and their relationships." ?

Honestly, do not think so, but its close enough.

There's a well known quote from Fred Brooks too:

""Show me your flowcharts and conceal your tables, and I shall continue to be mystified. Show me your tables, and I won’t usually need your flowcharts; they’ll be obvious.""

(Though from your description I suspect that's also not the one?)


Probably by Fred Brooks, as summarized by Guy L. Steele Jr in https://dreamsongs.com/ObjectsHaveNotFailedNarr.html:

> Fred Brooks, in Chapter 9 of The Mythical Man-Month, said this: Show me your flowchart and conceal your tables, and I shall continue to be mystified. Show me your tables, and I won't usually need your flowchart; it'll be obvious. That was in 1975. Eric Raymond, in The Cathedral and the Bazaar, paraphrased Brooks' remark into more modern language: Show me your code and conceal your data structures, and I shall continue to be mystified. Show me your data structures, and I won't usually need your code; it'll be obvious.


I'm glad we finally found the 1 basic tool of software design. We can for sure stop thinking and arguing about that.

I'm not sure if this is obvious to the average developer and I'm late to the show, but a few years back things clicked for me when I grokked that types exist no matter how you feel about them.

You still have to reason about the types no matter what. But writing it down empowers you to offload a lot of mental load to the computer, asking it to check your work, give you feedback as you go. It also communicates intent to other humans who read it.

One reason I like TypeScript is that there remains a, "just trust me" escape hatch when I'm testing/hacking something and don't want to "show my work" by writing out some complex type. (Funny how "show your work" is something math teachers have to fight many students to do. This feels like the same thing). But that complex type always existed. You're just saying "trust me on this." Writing it down doesn't conjure the type into existence.

Rust also clicked for me when I understood that it's really just more of the same thing: memory management, lifetimes, etc. exist no matter what. But it offers a way to show your work, formalizing intent, so that the checker/compiler can worry about the boring problems for you.

In this sense, the issue is really about making decisions on just how much "trust me on this" is appropriate.


Mereological Nihilism demonstrates that most types are an illusion.

Space, Time, Minds. Those exist. Probably :)


Python does have something like interfaces and it’s called Protocols https://docs.python.org/3/library/typing.html#typing.Protoco...

Other classes can count as implementors of protocols without needing to subclass them, and static type hints will notify you if you make a function which accepts a protocol as an argument and pass something which doesn’t implement it


There are still so many devs who don't want to deal with types, and they love Ruby and Python, as well as JavaScript without types.

It is quite difficult to work on large Ruby or Python projects; interfaces are not determined, and figuring out what's happening is painful.

Still, so many devs love it, and they wanna keep working with Ruby and Python in this way.

I kind of feel that experienced devs move on, and give up projects without types.


The funny thing is that even for devs who don't want to declare their types will still have think about them to some extent. Even if it's correcting run-time errors indicating that some function or property doesn't exist for type 'X'. Plus, they get to guess at the types that are expected for various function calls in their own projects. Oh sure, you can add comments that document it right above/below the function, but do you remember to keep those up to date while you're in the middle of refactoring?

These days most Python devs I know use types

This is stretching the definition of "tool." I would go far in agreeing that types are fundamental to the vocabulary of software design. But calling them a basic tool feels off. Is akin to saying rooms or units are a basic tool of building design. I can see arguments for it, but tools typically refer to the various things that actively do something. Hammers and such. Maybe a level bar?

> This is stretching the definition of "tool." I would go far in agreeing that types are fundamental to the vocabulary of software design. But calling them a basic tool feels off.

Sometimes I wonder if this type of article is just a rehash of object-oriented programming articles from the 90s where the keyword "class" is search-replace'd by "type".


(2018) btw.

I saw this much earlier in the day and dismissed it. I just now read more of it. I was first off-put by the title, but then it goes on to say:

> [...] design is about everything that’s leftover after we remove all the function bodies. It worth taking a moment to think about what that looks like. Here are some of my own immediate thoughts: 1. Everything that’s left is just types. [...]

Which reaffirms my suspicion that "Types!" are this writer's hammer for all of programming's nails, or possibly a click-bait writing device. There's probably a better article in here trying to get out as the end notes mention. [Maybe read them first.]

But I couldn't even agree with the first one:

> I meant for the main point of today’s essay to be universal: hence my emphasis that types are central to design even in dynamically typed languages. [...]

I can't say I've ever considered types/signatures when sketching out a design. That comes much later in prototyping and actual implementation--I suppose if I used Haskell that might be different.


I find this really hard to believe to be honest. Even dealing with things as rudimentary as a file handle are really thinking about a type of some abstract thing, even if you don't really conceptualize it as such. Types are the way to put a name on that abstract classification, so in dynamically typed languages even the name of a variable or function assigns a classification to the data it holds/returns, even if it's only really enforced by the programmer.

There are basically no untyped programming languages for a reason, types are a fundamental concretization.

In fact, I would argue there is no such thing as untyped programming, there's just different degrees to which the programmer is the enforcer of correctness.


I am skeptical. You don't even know if a relevant piece of data is say a number or text before you start writing out code? You don't think at all about how you will group data together? You don't think about the operations that will be performed on that data and how they will transform it?

What do you think about when it comes to design?


Location of data, policies, cross-cutting concerns, latency/performance requirements, etc.

“Much later” is probably an exaggeration. You get to types “soon after” the initial design.


Types are a useful design tool to explore the problem domain first, while the rest are details that emerge from that. Sure, if one starts with architecture, you're right. The software architect needs to consider these first. The programmer on the other hand ideally expects those as inputs.

When tasked with wearing on multiple hats (architect, programmer, user), I agree with your point. From a programmer's point of view, however, (abstract) types are IMHO indeed a much more useful tool as they cover many of the other concerns (policies, performance, etc.), too.


In my experience, a software engineer wears all the hats nowadays. Even crossing into product design and user experience.

It's interesting that you think about those things without even understanding how you are going to solve the user's problem first. Aka functional requirements.

Sure, I think the author overstates his/her case and "finds the One Ring". But it's not a dumb article. "Design is everything that's leftover after we remove all the function bodies" seems like a useful perspective.

Except it's not. The word "design" originated from the notion of marking out or pointing out. To do it well is to take the problem apart into small challenges that can be solved, with a plan on how to make the pieces hang together again.

Design often includes algorithms, which don't devolve merely to types. So this statement only makes tangential sense if all you're ever building is CRUD applications.


I pretty much always start with types/signatures when sketching out a design. I guess if there is a complicated algorithm, I might start with pseudo code for it, but even then, I probably want to start with what it's inputs are and what it's outputs are.

I’m reading Type Driven Development with Idris and this post really resonates with what I’m learning. Especially the idea of writing down the function’s type to be guided how to implement it.

PS: Type Driven Development with Idris is mentioned in the final notes. OP, I could not recommend it more!

Our 1 mln line legacy project was written without type hints, often preferring arbitrary arrays with keys, and so:

1) despite having tests, almost every day there's some bug in production because code expects data X but it receives data Y - this class of bugs is entirely non-existent in strict/static languages

2) to have some degree of control, they added a linter which infers and validates types, but it doesn't catch everything (see #1) and this step takes like 5 minutes - defeating the whole idea of "dynamic languages don't need compilation so you're faster with them", because modern static compiled languages can compile incrementally in a few seconds and you already have 100% type information without waiting for 5 minutes each time

3) refactoring code is very hard, because without proper typing, it's hard to find all instances of usage of data X or property X without manually researching tons of code (data flow); in languages with proper typing, IDE can find it all immediately, but ours fails to catch a lot of stuff

So in the end, in my experience:

1) lack of types allows for quicker debugging/hotfixing if you don't care about data/logic validity (because you don't run tests or linters anyway)

2) you waste more time and suffer more in the rest of the situations

So we started adding type hints all over the place, and banned arbitrary arrays with "magic" keys in public APIs.

I sincerely don't understand people who push against using types, it's like they never worked on large projects in teams, or never maintained a project for more than a few months. Or maybe they have some secret sauce I'm not aware of?


Grammar is the basic tool of writing. Atoms are the basic tool of physics. Etc.

What I think this post is reaching for is not types, and it’s beyond even abstract data structures. It's really templates, which is a general concept that gets used in every design scenario. Numbers are templates, integers are templates, bits are templates. We can describe how the idea works to each other and partly to the computer but it’s up to the programmer to animate them.

Once you understand the concept you see it everywhere. Super Mario is full of templates: you jump on an enemy’s head, or sometimes it doesn’t work but gives you a clue to do something else. Templates are nodes of understanding.


I’ve lost you at the template part: do you mean _models_?

> One of Haskell’s most basic innovations (well, I’d guess it probably wasn’t first, but compared to other relatively mainstream languages) was the ability to write the type of a function separately from writing the function body.

Anyone familiar with the initial versions of C / C++? Both languages have offerd the ability to define your function signatures in header files for as long as I have known them, so if you wished to separate from the actual function body you should have been able to do that.

But then Haskell can hardly have been the first mainstream? But maybe someone with more ancient knowledge could share some insights - thx.


I wouldn't characterize a forward declaration of a function as a distinct definition of its types—any types in the forward declaration must match those in the implementation; there's no way to implicitly refer to the types used in the forward declaration to ensure a single place of typing.

Arguably COBOL has it with its data division, as does Ada.

Common Lisp supports something along those lines, mostly for optimization purposes.

Could generalise it to 'data is the basic tool of software design', then the article would have to muddle shape, structure and type into a single type concept.

A lot of people are complaining that this seems too "obvious" in one way or another, but I think it can be useful to write down and be explicit about such an important underlying fact of programming. Completely understanding the types in your program forces you to think carefully about the domain.

I enjoy coding in assembly language, where types are not really a thing. Yeah, it really forces you to think carefully about the domain. I'm not talking about a little bit of in-line assembly, I've written entire UI, device drivers, and everything else in assembly - many thousands of lines of code. Maybe this is why I don't feel like Typescript has solved anything much for me when I work with front-end code.

> Completely understanding the types in your program forces you to think carefully about the domain.

Isn't it the other way around? Types derived from analysing the problem domain? That's the basic premise of a bunch of software design techniques.


Yes, understanding comes first and types come second. But being able to comfortably create types for your domain is a good sign you have sufficiently analyzed it.

> One of Haskell’s most basic innovations [cut] was the ability to write the type of a function separately from writing the function body.

I don't get it, what's the difference between this and C's function declaration?


In C, you still need to write the types in the definition.

Is it too simplistic or abstract to think of types as merely contracts?

Surely, however from a software architecture perspective types as contracts, assuming pure functions and maximally restrictive types, is a good model for reasoning about a system. A function such as:

addIfEven: Int -> Int -> Maybe Int

even without knowledge of the implementation lets us know our caller has 'entered into a contract' where addIfEven will reduce two integers depending on some conditions. This contract then becomes two-wayed: If I modify the implementation of the function there's a good chance I'll be forced to modify the signature, thereby letting me know statically that I've broken the contract with the callsite.

I'd recommend checking out https://lexi-lambda.github.io/blog/2020/08/13/types-as-axiom... for a far better write-up than this comment on why type-driven development is like unlocking a cheat-code for development.


Interesting opinion I've not heard before, or to add to your point then basically types are merely just "interfaces".

I think that works!

Denotational Semantics is the basic tool of software design.

Meaning, model the whole thing in math, then you know whether the implementation matches the design.


Author is dead wrong, just enamored with functions.

Functions can be seen as basic tools of software design only if you completely leave out IO and asynchronous nature of the communication.

That’s why Haskell is so beautiful and understandable when you work with pure data, and suddenly becomes so cumbersome and cryptic when you need to work with IO and async communication. Because functions ARE NOT basic tools of software design.

Actors that asynchronously pass each other messages are.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: