The type system is a programmer's best friend
dusted.codes
dusted.codes
Every programmer who has ever single-handedly written a 100,000+ LOC software system will tell you the same thing: shift as much responsibility on the compiler as you can and have the compiler check the code you write to any extent technologically possible.
Getting rid of bugs by experiencing, diagnosing and fixing them takes at least ten times more effort than getting rid of bugs by not making them in the first place, through expressing the problem at hand with a strong type system.
When you also consider the never ending necessity to introduce change to an already written software system, thus the necessity to refactor code (in the sense of altering the previously assumed meaning of its idioms), the critical advantage of a strong type system becomes self-evident.
(Yes, Rust 4ev3r! ;))
I was listening to the John Carmack episode of Lex Fridman from the summer, and he makes a comment about being frustrated that in the Valley there’s an almost religious opposition to IDEs, debuggers, and static analysis.
Some of those tools have only become more powerful over time and I’m perplexed as to the mindset that would make a person averse to automating the drudgiest parts of their job in a career that is almost entirely based around automating things.
I use (a version of) visual studio, the first thing that I did was installing a vim extension.
Also vim itself already supports almost any of these features with some basic plugins for the completion logic. If I type "." in my vim I get the same thing I would in visual studio. If you are saying "vim is bad because it lacks X IDE feature" you are missing the point.
VScode: Of course I can do autocomplete bud. Here, search my package repo, I’ll tell you which plug-ins are the most popular and handle the entire download and install process for you.
There’s no comparison.
Kudos on the people who have ported this to Vim[2], but I suspect the support for LSP features will still be better in VS Code
LSP is cool though, an advancement certainly, but it is not a completely new thing.
These days, I use Neovim + LSP for a pretty decent approximation of an IDE-- it's quite good. Still not as good as Visual Studio + C#, but I'm on Linux now, and not writing C# anymore, and I definitely prefer an open-source, general purpose, light-weight, customizable editor.
Now, for C or Java or most other popular languages, you're absolutely right.
I can't speak for Vim, but Emacs has stepped debugging, code completion, etc.
Part of the reason I use Emacs is that I get those features and (and a ton more) with the same light weight interface across multiple languages and platforms. At the same time, it's usually very easy to make Emacs work with third-party tools. On Linux I can step through code using GDB through Emacs, but at work I spend most of my time in Emacs, but have it bring up the MSVC++ debugger when I need it. It's the best of everything.
Meanwhile, I get to listen to my coworkers celebrate new features in VSCode that I've used in Emacs for years...
There are no bonus points for learning a new IDE for every project, either.
I'm confused. The point of uber-IDEs (IntelliJ, Eclipse, NetBeans, VSCode): You only need to learn one IDE. Each language is just another plug-in.
Why do you think they are doing things "the hard way"? Emacs can have all the things you have described. And it can do that for languages that are not part of the .NET Framework, they just have to have a language server implementation.
The problem with "IDEs" was placing all your eggs into a single basket. You are on Visual Studio and then you need to do some Java(or Scala, or whatever). And now you have to get another IDE. Some tried to be one IDE to rule them all (Eclipse, Netbeans), didn't work all that well.
You need a good editor - most IDEs don't have one. You need integration with your language of choice. You need compilers and linters and a bunch of other things. Better to glue components that do one thing and do it well, than having one IDE trying to do everything. And those components can fell just as integrated.
In the first few years of my career I would try every few months to get these things actually installed and working in emacs. It was definitely the hard way.
>The problem with "IDEs" was placing all your eggs into a single basket
Eclipse is a uniquely terrible piece of software and I understand why it might polarize people against IDEs for life. The JetBrains products are pretty good and, importantly, modular and consistent enough across languages that I don't mind.
Are you objectively more productive, or do you perceive yourself to be more productive?
I was an Emacs diehard for nearly two decades. Then I got introduced to CLion. In retrospect, Emacs was an inferior tool.
While it's certainly true that Emacs can be configured and extended to do all sorts of interesting things, I do suspect many of its users are deluding themselves when it comes to the sheer breadth and depth of functionality offered by a modern IDE. It also doesn't help that its maintainers are living several decades in the past, and haven't kept up with what the "competition" are doing.
I'm certainly not implying that Emacs is a poor editor. Very few IDEs have text editing capabilities on par with that. But its integration with other tools doesn't hold a candle to today's IDEs.
Perhaps not from the technical perspective, but there are social advantages if you are a member of a clique that does things the hard way and considers it a sign of competence. ("Any idiot can write a code that compiles using autocomplete and syntax highlighting, but it takes a true master to achieve the same result using a decades-old Linux equivalent of Notepad.") They don't seem to understand that the ancient masters did things the hard way not as a pointless exercise, but simply because the easy way was not available back then.
IDEs have some use, and static analysis has proven to catch the same mistake over and over, but only as the authors of those tools have discovered that false positives cannot be allowed ever, once there is a false positive the tools is worthless.
Yes. I was kind of forced to think and do printf debugging at the beginning of my career, after having used before that (as a hobbyist) quite good asm debuggers.
Maybe that's just me, but I also was, I believe, a bit over-reliant on the debugger - I would just compile, run, see what happen, and launch the debugger if something didn't work.
Nowadays I could use sometimes a debugger - but the system I work with is sort of soft real-time so stopping at a breakpoint of even slight changes in timings can change the context - in some cases even printf debugging could make a bug vanish.
If nothing else, debugging without a debugger is a good exercise in logically thinking - and it can save time too.
Talk about progress.
Glad that’s over.
No matter how well that's done, it creates friction somewhere in the process interacting with existing code.
That is, I can agree that an inherently strongly typed language has benefits, while also being skeptical about bolted on additions.
A big chunk of my job is writing node microservices in AWS Lambda. I do everything I can to avoid shared library code, since past experience tells me there be lots of dragons (mainly in when and how to push or pull lib updates to components). I have a very tiny shared lib that I try to never touch and definitely never introduce breaking changes.
Unit tests are a breeze since I never have to cast objects or worry about generics, etc.
Typescript would slow me down so much and add absolutely no benefit. Maybe I'm misinterpreting though and no one is claiming Typescript would benefit here.
We also have some C# lamdbas and I find writing unit tests for those so much more of a pain - since the shared libs have generics and I'm always casting things. But admittedly I don't know all the tricks.
Generics reduce the amount of things you must care about on your tests.
And you shouldn't cast objects in almost no code ever. Most 100k LoC programs won't need it even once, your microservices should need it proportionally less.
That's the thing. The gains grow superlineraly with the amount of code. They make it just a bit easier to write some trivial 100's LoC programs, and they make it possible at all to have a working 100k LoC system. But if you don't learn them, you won't know where the break-even point is for you.
That's a good way to put it.
- Define all app state as a snapshot. Problem: snapshot can become stale, so more infra might be necessary to make sure that snapshot is up to date;
- Pass only the necessary state and construct as necessary. Problem: hard to define whole state precisely and ensure that it conforms to runtime state of a healthy app;
- Pass a subset of necessary state for some execution branch and cast the type. Problem: casting may result in test failures during runtime and potentially other issues such as modify-run-fail debug loop;
- Mock return values of functions called within the function being tested and use any combination of "state passing options above".
In a lot of places I use such approach with custom type helpers and transitive types, and passing in only the necessary subset for smaller functions or mocking return values for bigger ones. What do you think? I know that the AppState can be defined as a union of possible states and together with type guards can address those issues better. I just wanted to hear your opinion on how you would address such problems. I hope I explained it well enough.
export type Fn = (...params: any) => any;
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
export type FirstParamType<G> = G extends Fn[]
? UnionToIntersection<Parameters<G[number]>[0]>
: G extends Fn
? Parameters<G>[0]
: never;
export interface AppState {
first: {
a: number;
b: number[];
};
second: {
c: string;
d: string[];
}
}
type DeepPick<A, B extends keyof A, C extends keyof A[B]> = { [BK in B]: Pick<A[B], C> };
function calculateUsingFirstB(state: DeepPick<AppState, "first", "b">): number[] {
return state.first.b; // some calculation
}
function calculateUsingSecondC(state: DeepPick<AppState, "second", "c">): string {
return state.second.c; // another calculation
}
// function which takes complex state parameter and calculates the result based on results of other functions
function calculateMore(state: FirstParamType<[typeof calculateUsingFirstB, typeof calculateUsingSecondC]> & DeepPick<AppState, "first", "a">): string | number[] {
if (state.first.a > 10) {
return calculateUsingFirstB(state);
}
return calculateUsingSecondC(state);
}This indicates to me that you're trying to write code that isn't correct (not doesn't work, but rather only works because of implicit couplings between components) and/or doing exotic lisp-style metaprogramming.
In the latter case, yeah, C#'s type system isn't powerful enough. Others are (to an extent: arbitrary code execution at compile time is never going to be completely safe).
In the former case ... that should be difficult. Forcing you to be explicit is half the point of a type system.
Probably there's a better way. I'm not a C# expert. But I was a Java dev for 10 years so it's not super unfamiliar.
I'm a big fan of WET - write everything twice. Then maybe the 3rd of 4th time worry about creating some new abstraction to share code. It's so much easier to add an abstraction later when it becomes obviously needed - than to remove one when you find out your three things actually need different variations on the lib code.
That's to say, having old conversations with new engineers is a really refreshing exercise and I'd encourage the world to continue doing it.
I think that people first starting out or people who have never worked on large/new code bases with a diverse range of authors don't seem to appreciate this concept.
Rust has a perfectly nice type system by modern standards, but it's nowhere close to showing you just how deep the rabbit hole goes when it comes to avoiding bugs at runtime by having stronger type systems.
For example suppose my Rust function takes a slice of clowns (named unimaginatively "clowns") and also a usize integer k. Can we write clowns[k] ? Rust says sure, it will emit a runtime bounds check to confirm that k is inside the bounds of the slice. If there are sixteen clowns, and we ask for k = 20, this Rust code will panic at runtime.
But we can do better, if we are willing to pay for it. Dependent Types. In a language with dependent types and enough inference our type inference system will conclude that k can be 20 here, thus clowns must be a slice of at least 21 clowns, but this slice has only sixteen clowns - type error during compilation, either k or clowns are wrong.
Now, for cases where bounds checking would be the reasonable thing to do, Dependent Types just result in you writing bounds checks, ie in this case checking k < 16, and so it's possible you will just end up doing more work to result in a program that still just says, at runtime, "Nope, not enough clowns" or whatever like in Rust. The type system will require you to write correct bounds checks, but the Rust bounds checks are auto-generated, so they're correct too.
But in cases where bounds checks were not the only sensible approach, or maybe you didn't even realise a bounds check would be emitted because you assumed it was statically correct - this can catch some bugs at compile time which would otherwise survive into a running program, "Shifting left" is I believe the usual phrase to describe this improvement.
If you thought the function is obviously correct, "Of course there are more than k clowns" but it isn't, the type error may cause you to take that extra moment to think about it. "Wait, why can there be fewer clowns than... oh, I didn't mean clowns here, this should say circus_performers. I'm not even using the right slice!".
I disagree -- Rust's type system is pretty weak and very limiting compared to other modern languages like Typescript, Nim, Zig, or even C++. That's without even without getting into dependent type languages.
There are so many basic patterns which Rust's type system can't handle, especially when it comes to compile time types. For example, one recent thing I ran into was trying to downcast a dyn Trait into another more specific dyn Trait. It's just not possible, at least on stable. Instead you have to do ugly work like getters that return sub-traits wrapped in optionals. Ick, the visitor pattern was easier.
Rust's type system philosophically takes a very strict "closed system" approach too. Meaning one can only really write code that's valid for every known instance. This limits entirely valid and useful subset of programs to programmers using libraries or language features. I'm not talking about runtime dynamism, but compile time dynanism. C++ with concepts provides a much more powerful and adaptive type system.
Programming in Rust frustrates me that I can't do any of the compile time checks that I've become accustomed to doing in other languages. It makes doing them at runtime difficult as well.
> but it's nowhere close to showing you just how deep the rabbit hole goes when it comes to avoiding bugs at runtime by having stronger type systems.
Totally agree on that! Though that's missing how powerful compile time programming in general can be even without the dependent types. Personally I'm excited to see how C++ Concepts can evolve. Though I'm not sure how much that starts overlapping with Dependent Types.
So, C++ 20 Concepts is basically what Bjarne Stroustrup proposed for a future version of his C++ language in the early 2000s. Several people proposed and WG21 accepted, a far more capable feature set for Concepts, this is often referred to as C++ 0x Concepts, since it was accepted for C++ 0x, the standard that would eventually (after years of delays) become C++ 11.
Bjarne wrote a paper arguing that this more powerful feature set was unnecessary and perhaps unworkable, and WG21 wound up removing Concepts from C++ 11 entirely (and the people behind it mostly got the message and ceased working on C++ altogether). A decade later, something that's close to Bjarne's original proposal became a C++ 20 feature.
C++ 0x Concepts was similar to Rust's Trait system in many ways. Particularly notable features of C++ 0x Concepts you might recognise in Rust's Traits:
1. Third parties can implement a C++ 0x Concept for some type which was not originally conceived with this Concept in mind, they just write the implementation and it works.
2. C++ 0x Concepts must be explicitly implemented they're not just a syntactic requirement that could be satisfied by happenstance in a type which is not in fact suitable.
3. As a result of 1 & 2, the C++ 0x Concepts have Semantics which in C++ 20 Concepts are confined to the idea of "modelling" a Concept or else IFNDR.
https://xenaproject.wordpress.com/2020/07/05/division-by-zer...
https://www.cs.ox.ac.uk/ralf.hinze/WG2.8/26/slides/xavier.pd...
Ruby and Python overtook Java and C++ in the early 2010s in _spite_ of their lack of a good typing system, not because of it. On the whole, they are much more productive languages.
Now we're seeing languages that have Ruby / Python productivity but also have much better ways of static typing such as Typescript and Swift. And the Ruby / Python community is more open to static types as well.
The problems of ~2010 Java and C++ were mistakenly pinned on static types and the framing of "static vs dynamic languages" was always a red herring. Java and C++ were just crappy languages (at least in 2010, not sure about modern incarnations).
It really is a shame that Swift is so confined to the iOS world because it's such a great example of how you can have a language that feels like a scripting language but with much more advanced type safety.
There are great examples far older than Swift, for example ML that dates back to the 1970s, or OCaml that's as old as Java.
At the end of the day, the only thing that matters is writing something that works and works well. Hackers go through too many moodswings to be worth paying attention to when they start telling you how you should code.
True! But computer scientists (who are sometimes also hackers, sometimes not) apply research methods to existing codebases and the practice of coding, and have repeatedly presented findings that indicate that, mood swings/fads/hype cycles aside, some techniques really do deliver better software quicker. The VPRI STEPS work is an interesting example of this.
That's not to say that every CS methodology paper should be taken as gospel; we have problems just like other disciplines, sometimes more. But it's a far cry from post-hoc rationalization and hacker mood swings.
"Back in the day..." I worked on a collection of vital (to the company) infrastructure apps written in Borland Turbo (object) Pascal. Strong static type checking was enforced by the language. Good type design and strict type checking meant that it was normal that when a program compiled, it was bug free!
Much as I enjoy the flexibility of python, I know that every refactor or significant change means that there are now execution paths that have not been exercised - the burden of comprehensive testing is enormous, far outweighing the convenience of dynamic typing.
What I’m trying to say is that we should be open about another concept, for example contract-based programs (clojure’s spec for example), because they might have better properties.
Why does 100,000 lines of code of python tend to be safer and more manageable then 100,000 lines of C++ despite the fact that python has no type checker and C++ has a relatively advanced type checker?
Why do startups choose a python web stack over a C++ web stack?
I don't think it's "self-evident." I think there's something more nuanced going on here. Hear me out. I think type systems are GREAT. I think python type hints and typescripts are the way forward. HOWEVER, the paradox is real.
Think about it this way. If you have errors in your program, does it matter that much if those errors are caught during runtime or compile time? An error in compile time is caught sooner rather then later but either way it's caught. YOU are protected regardless.
So basically compile time type checking just makes some of the errors get caught earlier which is a slight benefit but not a KEY differentiator. I mean we all run our code and test it anyways despite whether the system is typed or not so the programmer usually finds most of these errors anyways.
So what was it that makes python easier to use then C++?
Traceability and determinism. Errors are easily reproduced, languages that always display the same symptoms from certain errors and in turn deliver error messages that are clear and are readable. These are really the key factors. C++ on top of non-deterministic segfaults, astonishingly even has compile time messages that can confuse users even further.
It's a bit like saying that there's a paradox: everyone says that flying is safer than driving, but experimental test pilots die at a much higher rate than school bus drivers!
Much of the fervor around dynamically typed languages in the past was driven largely by the dichotomy between c++ and other dynamically typed languages.
Nowadays it's more obvious what the differentiator was. But the point im making here is that type checking is NOT the key differentiator here.
Unfortunately, I have to completely disagree here, at least based on my experience. Shifting software error detection from runtime to compile time is absolutely paramount and, in the long run, worth any additional effort required to take advantage of a strong type system.
Firstly, writing unit tests that examine all the possible combinations and edge cases of software component input and state is... an art that requires enormous effort. (If you don't believe me, talk to the SQLite guys and gals, whose codebase is 5% product code and 95% unit test code.)
Secondly, writing automated UI tests that examine all the possible combinations and edge cases of UI event processing and UI state is... next to impossible. (If you don't believe me, talk to all the iOS XCUI guys and gals who had to invent entire dedicated Functional Reactive paradigms such as Combine and SwiftUI. ;) J/K)
Thirdly, I don't even want to get into the topic of writing tests for detecting advanced software problems such as memory corruption or multi-threaded race conditions. Almost nobody really seems to know how to write those truly effectively.
> So what was it that makes python easier to use then C++?
The Garbage Collector, which is side-stepping all the possible memory management problems possible with careless C++. However, a GC programming language probably cannot be the tool of choice for all the possible problem domains (e.g., resource-constrained environments such as embedded and serverless; high-performance environments such as operating systems, database internals, financial trading systems, etc.)
Otherwise, your post is excellent. Lots of good points. SQLite is something that EADS/ESA/NASA/JAXA would write for a aeroplane / jet fighter / satellite / rocket.
SQlite is written in C. It has type checking. Yet people still write unit tests for it. Why? Because type checking is mostly practically inconsequential. All your points don't prove anything. It proves my point.
All the problems you talk about can be solved with more advanced proof based checkers. These systems can literally proof check your entire program to be fully in spec precompile time. It goes far beyond just types. Agda, Idris, Coq, and Microsofts lean have facilities to prove your programs to be fully correct 100% of the time. They exist. But they're not popular. And there's a reason for that.
You say it's paramount to move error detection to compile time. I say, this problem is ALREADY solved, but remains unused because these methods aren't PRACTICAL.
So that makes your statement contradictory. You think type checkers are important but you think python works because of garbage collection.
Either way I'm not talking about the implementation of the language. I'm talking about the user interface. Why is one user interface better than the other?
I bet you if c++ has sane error messages and was able to deliver the exact location of seg faults nobody would be complaining about it as much. (There's an implementation cost to this but I am not talking about this)
Even an ugly ass language like golang is loved simply because the user interface is straight forward. You don't get non deterministic errors or unclear messages.
Of course it matters. If an error can be caught by the compiler, it will never get to production. Big win.
With typeless languages like python the code will get to production unless you have 100% perfect test coverage (corollary: nobody has 100% perfect test coverage) and then some unexpected moment it'll blow up there causing an outage.
This happens with metronomic regularity at my current startup (python codebase), at least once a month. It is so frustrating that in this day and age we are still making such basic mistakes when superior technology exists and the benefits are well understood.
Something like c++. You get a runtime errors. You have no idea where it lives or what caused it.
Your python code base delivers an error but a patch should trivial because python tells you what happened. Over time these errors should become much less.
It depends on how you define "safer." Run-time errors with Python happen frequently in large programs due to poor type checking all the time. Often internal code is not well documented (or documented incorrectly) so you may get back a surprise under certain conditions. Unless you've have very strict tooling, like mypy, very high test coverage, etc. there is less determinism with Python.
Also, this may come as a surprise, but many people do not run or test their code. I've seen Python code committed that was copy-pasta'd from elsewhere and has missing imports, for example. Generally this is in some unhappy path that handles an error condition, which was obviously never tested or run.
Statistically more errors are caught by python runtime then an equivalent type checked c++ program simply because the python user interface fails hard and fast with a clear error message. C++ on the other doesn't do this at all. The symptoms of the error are often not related to the cause. Python is safer then C++. And this dichotomy causes insight to emerge. Why did python beat c++?
In this case the type checker is irrelevant. Python is better because of clear and deterministic errors and hard and fast failures. If this is exemplary of the dichotomy between c++ and python and if type checkers are irrelevant in this dichotomy it points to the possibility that type checking isn't truly what makes a language easier to use and safer.
The current paradigm is rust and Haskell are great because of type checking. This is an illusion. I initially thought this was well.
Imagine a type checker that worked like c++. Non deterministic errors and obscure error messages. Sure your program can't compile but you are suffering from much of the same problems, it's just everything is moved to compile time.
It's not about type checking. It's all about traceability. This is the key.
>there is less determinism with Python
You don't understand the meaning of the word determinism. Python is almost 100 percent deterministic. The same program run anywhere with an error will produce the same error message at the same location all the time. That is determinism. Type checking and unit testing does not correlate with this at all.
This is not the case with c++.
You can compare however with a Java or C# web stack, both of which offer a superior developer experience, as well as a superior production experience (monitoring, performance, package management, etc.).
I compare C++ and python because I'm trying to make a statement about types. How types are actually not as important as people think.
C++ and python are good examples because python is safer and easier to use then C++ yet python doesn't have type checking.
But why it NEEDS that?
Because C++ is FAR MORE DANGEROUS.
And worse language, in so many aspects, that you need everything to tame it.
In contrast, other langs like python have the luxury of see what C/C++ do wrong and improve over it.
Just having a `String` type, for example, is a massive boost.
So for them, the type system already have improved the experience!
---
So this is key: Langs like python have a type system (and that includes the whole space from syntax to ergonomics - like `for i in x`, to semantics) and the impact of adding a "static type system checker analysis" is reduced thank to that.
And considering that if you benchmark for a "static type system checker analysis" is what C++/C#/Java (at the start?) is then the value is not much.
Is only when you go for ML type systems where the value of a static checker become much more profitable.
Simple type systems can also offer equivalent safety with less flexibility. What make Haskell seem more safe is more the functional part combined with type safety. Functional programming eliminates out of order errors where imperative procedure were done in the wrong order.
The passengers on the fly-by-wire jet running the program might well say that it matters.
C++ is chosen for speed. Not for safety. The amount of run-time and compile time checks C++ skips is astronomical. The passengers may think it matters, but the programmers of those systems by NOT using a program that does compile time or run time checks are saying it doesn't matter.
Because C++ sucks, but static types are not to blame for that.
I'm saying type checking is not one of those things.
Developer productivity in these scripting languages is also even higher. Simply because of how much faster they are to program in with the code then run/test loop. Java and C# can have loong compile times. Python and typescript are typically much much more quicker. With the additional type safety python and typescript are actually categorically higher in developer productivity then C# or Java.
But that's besides my point. Let's assume we aren't using modern conventions and javascript and python are typeless. My point is that whether or not C# or java crushes python and javascript over maintenance it doesn't win because of type checking.
No the hell you don't.
Please please please do not attempt to separate the alias from an email address I submit. It's there for a reason - specifically, to hold you accountable if I experience a sudden influx of spam, and generally to keep things categorized in a world where senders can be sending things from all sorts of domains. Knowing that this is something one would even remotely consider is grounds to never touch anything one has built with a ten-foot pole, and I am now very strongly inclined to look into the author and compulsively scrub any accounts of mine from anything said author might've touched.
I am not exaggerating. The thing before the @ is meant to be opaque. Deeming otherwise for the sake of something so blatantly user-hostile as removing aliases is plain evil, and I will not sugarcoat my condemnation of such practices.
If you're sufficiently sociopathic to have no regard for the morality argument here, then at the very least take heed of RFC 5322 (https://datatracker.ietf.org/doc/html/rfc5322) and recognize that trying to parse any meaning from an email address' local-part is blatantly ignorant of IETF specifications and almost certainly will create bugs. Just don't do it - if not for your users' sake, then for your own.
I am sorry but this makes no sense. You do realize that the only reason you are able to use aliases is because your email provider chooses to parse meaning out of the supposedly "opaque" text right? If your email provider is free to "break" the spec, so are people you give your id to.
EDIT:
> If your email provider is free to "break" the spec, so are people you give your id to.
Wrong. See above. The email provider is free to "break" the spec because it is the thing in control of that email address and can therefore process it as it sees fit. The people to whom I give an ID are not my email provider, and therefore do not have the same degree of control; consequently, attempting to parse meaning from that opaque string will cause bugs, and also is a dick move which will not be tolerated.
If you're defending this practice because you, too, are parsing the opaque components of email addresses which you do not control, then I will take note to look into your code contributions as well and avoid anything you've touched.
Do. Not. Parse. The. Local-part. For. Aliases. Full stop. It's my email address, not yours. Respect how I enter it, or else remove it from your system entirely. Anything different is asking for bugs and is blatantly disrepsectful to users.
There is no reasoning behind this argument; it is purely a verbal construct memetically derived from some inapplicable equality ethic that might make sense in a completely unrelated situation.
The correct application of ethics is that someone agency who is given abc+def@gmail.com, and infers from it that this gives them permission to send email to abc@gmail.com (or, worse, sell that address to harvesters) is behaving unethically.
Of more interest to me, omitted from the presentation--as almost always--is anything about what is disliked about a malformed address. You see this when some web form says it doesn't like your address, but won't say why, leaving you to guess and try things until it is satisfied.
Another example is the password filter that idiotically demands "at least one capital letter, one digit, and one swear character" in your already several-word passphrase, and dislikes your choice of swear characters but won't say so.
For one, you shouldn't be concerned about that, and for two, you can't tell delivery uniqueness anyway, since someone can have multiple completely different addresses going to the same inbox.
I'm concerned about some service collecting my email address and "accidentally" exposing it to spammers.
> Of more interest to me, omitted from the presentation--as almost always--is anything about what is disliked about a malformed address. You see this when some web form says it doesn't like your address, but won't say why, leaving you to guess and try things until it is satisfied.
That is indeed yet another reason why you should never ever try to parse meaning from email addresses you do not own.
At issue here is only how much parsing is allowed.
1> (jp-hash "correct-battery-horse-staple")
"Pyochu1ponu*fuson"
https://addons.mozilla.org/firefox/addon/jp-hash/This ensures everything before the @ is opaque, i.e. foo+bar@gmail.com is now bar@foo.com
Services that block my domain are usually the ones that also block foo+bar@gmail.com
What's more, for a programmer who doesn't get the value of types, the major downsides are already apparent, at least at a basic level. Doesn't this make my code more verbose? Doesn't this get super confusing sometimes?
It's an article design to help certain programmers learn a particular thing, not an article meant to satisfy more experienced programmers' desire to see all sides of an argument acknowledged.
How could the downsides possibly be apparent if the upsides are so mysterious they need an article to spell them out?
> It's an article design to help certain programmers learn a particular thing
Have they actually learned that particular thing if they don't know the tradeoffs they're making? I would argue they haven't. You need to know what you're getting and what you're giving up before you can decide whether something is worth using at all. There are too many articles hyping the upsides of technology X, but nobody asking what the downsides are.
The trade-off is obvious: you gain confidence about your program but you need to Learn More Stuff. Nobody's talking about the downsides of type systems except to the extent that they're worth talking about: see the comments here every time someone compares the type systems of Python and Rust.
Besides, they aren't going print this article out and take it to a cave in the mountains to learn about type systems for a year. They're going to read other stuff along the way.
If I update a type, the compiler will tell me every single location where I need to make a corresponding code change. For grug: change type give red squiggle, make change code good
Also, grug makes a good point about the temptations of generics, but I think they’re exaggerating the impact to the speed of development.
I forgot about this, thanks for the morning laugh. No such thing as a free lunch.
I’m sure one could argue that member names should obviate the need for type annotations.
There’s also the distinct possibility that my preceding ~15 years of statically-typed software development have affected how I think about software development in some way. (wink)
But I am finding type annotations internet useful while working on a huge application that is about 2 years into adding a gradual typing system, enough so that I usually take time whenever I enter a new code area to add annotations to everything, just to understand what’s going on.
My perception is that I invest time to build understanding of the types, and then document what I’ve learned in the form of these type annotations so that future maintainers then gain a quicker understanding without having to do the initial research.
At least my non-statistically-significantly-sized team agrees.
At a syntax level perhaps, but not necessarily at a semantic level. This won't apply to everyone, but I've noticed that the more types are relied on, the less my co-workers really understand the code. They're relying on the compiler so much they don't slow down and think through the changes they're making. In one extreme case I saw a guess-change-compile workflow that relied entirely on the compiler doing the work for them.
There are only tradeoffs when we are at the frontier of what's possible with a set of technologies, and so must trade off on something in order to move along that frontier[1]. Many languages aren't operating at that frontier, and adding static typing is free (in the marginal case, ignoring the substantial effort to implement the type system). If you start a greenfield Python project, and you start typing right away and incorporate MyPy into your CI and IDE - it's as close to free as you can get, and the benefit is substantial.
[1] Eg, like in this diagram, http://image1.slideserve.com/2488675/production-possibilitie... - we only need to trade off on guns & butter if we're along the frontier (the blue line), if we find ourselves somewhere in the middle we can just make more stuff until we reach the frontier.
A way this often gets worked around is code generation. Anywhere you have or reach for codegen in a static language, you probably could have used a plain old function in a dynamic language.
I think this is a dangerous position to take to extremes/as an axiom.
Not talking about type systems at all here. The assumption that, given two tools/techniques for accomplishing the same goal, there are always equivalent tradeoffs simply isn't true.
Some tools are better than others.
That statement usually provokes misinterpretation. It should not be taken to mean:
- That some tools are always better than others, in every context. There are situations in 2022 where COBOL is the best choice for new code, and other situations where rewrite-it-in-Rust is the best choice. Problems occur when "tradeoffs of tool A (even if we don't know what they are yet) make it equivalent to tool B" is a core tenet of decision making.
- That some tools always have been and/or always will be better than others. Context, expectations, tool capabilities, and available programmer talent pools all change massively over time.
- That one tool is better than all the others. Plenty of times there are multiple ways to deliver optimal-given-constraints outcomes, and it comes down to a matter of taste or "just pick something, anything, and let us get to work".
Chasing hype and cargo culting leads to poor outcomes; "we should build our two-core app on Kubernetes/write our 2TPS app in Rust" are often justified with "because it's the future" or "because the cool kids are doing it". That's a major bummer.
But the opposite extreme is just as bad: assuming that all choices are fundamentally a wash because "to get something, you have to give up something else" is just as methodologically irresponsible as following the hype cycle. Programming isn't alchemy. This kind of bad decisionmaking can lead to dependence on obsolete (unsupported/insecure) tools, difficulty hiring, and, at worst, a culture of "don't talk about Python to me; if you can't freehand it in C you just need to get gud" gatekeeping cruelty.
Everything has tradeoffs. That doesn't mean they're equivalent.
The issue is that people from one side will always downplay the tradeoffs (or pretend they don't exist, like the current author). This exactly how hype and cargo culting happens.
At this point in the trajectory of software engineering, it's fair to assume that most of the low-hanging fruits have been picked, and solutions that are unequivocally better would have to bring something fundamentally new to the table (which types are not at all). Most solutions will be picking a particular point on a trade-off isocurve.
Apart from that, it's always fair to ask someone who's strongly proposing something what the downsides are.
As for types, I’m not saying they’re flawless. I’m pointing out that in the transition from assembly to high level languages there were flaws and criticisms that came out. But looking back fifty years, were these flaws and criticisms genuine tradeoffs that kept assembly as a reasonable option for most developers? No, they were not. Now we look back on programmers who insisted on writing assembly as oddities, as niche figures. I cannot say if this will be the case for types, but I won’t rule it out.
Yeah they’re definitely programmers, but anyone who’s had to maintain and deploy what a data scientist came up with in a notebook will question whether they should really be using types.
We had conversion functions between them, and type inference when you performed certain operations.
The result was a disaster. Not an enormous disaster, but enough of a problem to rip the entire thing out and replace it with plain double-prevision floating points, and sensible variable names, everywhere.
Why was this? It was a combination of there being nothing sensible to infer when you, say, multiply an angle and a distance (which happens when you're doing algebra), and everything in the whole codebase needing to be aware of these types.
The downside of all these brilliant ideas is dependency. If you define an "EmailAddress" type, as the article suggests, you've got to write the code for it somewhere. Now all your projects are dependent on this library, with all the pain and anguish that versioning and linking/including/whatever the library brings.
Before, you depended on nothing but the String type, which is very likely built into your language. When all your code needed to do was pull that email address out of some persistent store (say), and send to some other piece of code, your dependency list was just the Persistence library. But with your fancy EmailAddress type, your dependencies are now much worse.
Keep things as simple as they can be. An EmailAddress type is not useful.
wtf? metres and centimetres are not different types! they are just different ways of writing same type: Length. radians and degrees are just ways of writing a dimensionless Angle quantity. you made the absolutely elementary mistake of conflating a physical quantity with the unit used to measure it, of course it was a disaster.
>nothing sensible to infer when you, say, multiply an angle and a distance
angles are dimensionless so they should just be a distinct type of float. there is literally no problem here.
But I'm not sure that's much of a tradeoff. If I don't know what type this thing is, how do I know what operations I can safely do on it? How do I know that I can make it do what I'm trying to do? Or will it blow up at runtime when I do that?
I consider "I coded it, it's done, but it might blow up at runtime" to be highly unprofessional. "We covered that with unit tests" is theoretically OK, if you've got 100% test coverage. But you don't, and you never will.
Having to say everywhere what the type is gets tedious. Autocomplete (and "auto", for those languages that have it) help a bit here, but only a bit.
https://hirrolot.github.io/posts/why-static-languages-suffer...
The key thing the author identifies is the two languages problem. Static types are a second program about the program, and that's great when the second program is simple declarations that keep you from passing one struct when another is expected.
But a flexible language needs more than that, and generics end up either Turing Complete or bad in some other way.
I'll be mulling this one over in years to come.
Usually moving from primitives into complex types does not account for serialization and deserialization between db and the client. This can be very annoying to work with in something like C#.
Usually it ends up resulting in alot more types and a lot more mapping between types.
However this has its own benefits, but is very boilerplate-y and is sluggish to work with when your domain changes.
Luckily, for C#, https://github.com/SteveDunn/Vogen now exists thanks to source code generators which soothes some of the issues.
As an example of this, in a piece of code I'm working on there's a Base64_String type, where only RFC 4648 characters are permitted to be part of the string, the '=' padding character can only appear at the end of the string, and if the second-to-last padding byte is '=' then the last one must be as well. This is all enforced by the type system without having to call "validate()" or something every time its used.
1. https://learn.adacore.com/courses/intro-to-ada/chapters/cont...
These are the sorts of cases with function parameters in various languages other language I've dealt with, in which this would have helped:
- "dt": delta time of what? Seconds, milliseconds, microseconds, nanoseconds, ticks? Usually, I'd expect seconds if it was a float, though I've seen counter-examples, and I usually have to trace back the flow to know for use if it's a 64-bit (u)int.
- "ip_addr" and "port": What's the type of port? If you guessed "int", you'd be right in part of the system. If you guessed "string" you'd be right in a different part of the system.
- "path": Does it matter if this is a relative or absolute path? It often isn't apparent this matters and then you find out this path is passed to a different system in which it does matter.
Rust having builtin IP address types (and libraries actually using them) is long overdue for mainstream programming languages
For types with invariants, you just add the `Invariant` aspect and then the type invariant gets checked automatically when passed as a parameter. Combined with built-in pre/post conditions, I've found that these sort of automatically inserted checks give me a lot of confidence, and allow significant embedding of conceptual and domain knowledge during development.
As with many tools, there are caveats. It's often surprisingly slow (so avoid using it on hot paths, or only turn it on during your testing/pre-production runs) and can't type-check everything (e.g. callables). But it's still pretty nice and requires minimal effort to use!
https://ada-lang.io/docs/arm/AA-3/AA-3.2/#324--subtype-predi...
Everyone thinks they are so smart validating emails, phone numbers, zip codes and all until their great design goes live and they discover that users in the real world do not follow their assumptions.
I have seen that happen again and again. No, if your idea of validating an email is more complicated than "should have an @ symbol", I guarantee you, there is counter example that will mess you pretty system up. Have fun scrambling to fix that ticket.
Oh you think you know how an address and zip code should look like? No, you don't.
Please people, just use strings and call it a day. Why do you like to suffer?
I mean, sure, using the type system to protect you from mixing up units can be useful. Everything in moderation. Primitive types are good types.
Picking on one of those points, suppose emails are just strings. What then prevents them from getting used as names, identifiers, and other unrelated data? Just your continued vigilance as a programmer and hoping that nobody ever carelessly names the field "address" so that mistakes can slip by over a series of devolution commits.
You might not know much about what an email is, but you know _something_ about how you expect them to behave and be used, and if you like the computer to automate your work it's not totally unreasonable to rebind the string type as an email type and require explicit conversions at the point of use to treat it as anything other than just an email. There's zero runtime cost, it's not much more code, and that class of bugs is greatly reduced except for at the boundaries of the system.
Maybe that effort isn't worth it, or maybe your domain changes fast enough you'd have a lot of churn, or maybe those bugs aren't too important, or whatever. Using types to help you reason about the things you do know and do care about can be a huge productivity boost though, so I wouldn't just write the whole technique off.
A lot of developers confuse types and validation.
> No, if your idea of validating an email is more complicated than "should have an @ symbol"
Then how is just using a string any better? Would you rather litter the entire codebase with validations that this is indeed a valid email or just do it once at the entry point?
I think excessive type system hacks are bad too but they are more often than not the problem of the language where its unable to express certain concepts naturally (see C++ template hacks).
This question can't be answered without additional context. What are the requirements?
But as a heads up, Map<string, JsonValue> couldn't be representation of all valid JSON documents. Lists are valid Json as well AFAIK. So I think this is making a point.
Why would you? Just use it. For most of parts of the program it's not relevant to the computation whether the string is a "valid email", whatever that means. It's a string.
`newtype Email = Email String` and `newtype Username = Username String` are just `String`s with guard rails.
from typing import NewType
UserId = NewType('UserId', str)
user_id: UserId
user_id = "abc" # Type check error
user_id = UserId("abc") # OK
# At runtime, a NewType is the identity function
s1: str = "abc"
s2: UserId = UserId(s1)
assert s1 is s2
Docs: https://docs.python.org/3/library/typing.html#newtypeRuntime: https://tio.run/##TU49D4JADN37K544CIkLupk4OBoTXXQ2CKdcDHeXts...
Type checking: https://mypy-play.net/?mypy=latest&python=3.10&flags=show-er...
About 90% of stuff I ever worked does email things here or there, and I never had the desire for some hyper complex abstraction with 10 pages of documentation just to store email. I also never experienced a bug that would have been fixed by this.
I mean if the user is putting in some bullshit there, the basic validation of type="email" or pendant if I am not working in HTML is gonna tell him that and after that he does or does not get his verification email. Boom, problem solved along with other one's.
Need just the domain? Well I'll use a damn 1 liner function with split() or something for this hyper specific use case, don't encumber me with the probably faulty abstraction of email some library designer cooked up 8 years ago just for this?!
I do use types when they are actually convenient, but these types-are-the-best-everywhere-all-the-time articles keep failing to convince me...
Opaque type aliases are great because they impose semantics. Even if I don't know how to validate an email, it's nice, at times, to distinguish a string which I suspect to be an email from all other sorts of strings.
Here's a subversion of GitHub's authentication (now fixed) where they assumed that "lowercasing domain name using English case rules is always fine and produces the same result" led to a vulnerability: https://dev.to/jagracey/hacking-github-s-auth-with-unicode-s...
Apparently John@Gıthub.com normalizes to xn--john@gthub-2ub.com but Gıthub.com normalizes to xn--gthub-n4a.com.
For now I will go back to forgetting that emails not covered by /[A-Za-z0-9.-+_]+@[A-Za-z0-9.-_](.[A-Za-z0-9.-_])*/ exist to preserve my sanity but this does confuse me.
The thing is a lot of things are reversed here, the reality is the way evolution is more sensible is to "generalize" from the bottom up, unless you are very sure from the start that certain types categories make sense for your problem. That's how things should always be, you can only generalize once you've worked through a problem and realized certain aspects are actually common in someway and can be connected somehow, and thus you can take a subset of data you have in your program and organize it into a type. The thing is I feel like that is even harder than thinking top-down (which I feel like is where the pendulum is swinging now) because that takes time.
The reason top-downers still do their thing is they feel their method is better is selective thinking (sorry, I know this is harsh) because they honestly discount the anger and fury people from the outside have dealing with their code and they ignore the amount of refactoring they need to do when their designs break. In fact, they love the churn really or at least merely accept it as a "part of development."
Abstraction in this case, mean, for future changes, i just need to change in one place, the Email type abstraction instead of searching and replacing every ussage of primitive email string.
Note that this makes your overall point stronger, not weaker.
Requiring b2b users to use their @company.com email is common and some b2b customers actually expect to be able to configure that. Another simple case is stripping out any "+whatever" from gmail addresses and to flag that sort of thing for other eomains so support can verify it's not a case of someone creating 72 trial accounts. Yes, rejecting users due to naive and incorrect validation is bad, but treating emails as entirely opaque strings isn't always an option either.
trial1@mypersonaldomain
trial2@mypersonaldomain
trial3@mypersonaldomain
unless you are writing an email server. Then you would need to do this properly.
In other words, validate the data that is in the domain of your application. If your app simply _sends_ email, then it's not your domain, and don't need to validate, as long as the receiving end of the email (aka, the email server) accepts it.
Maybe dynamic typing just isn't my thing and I need a new gig...
3.10 does add unions using | which is nice, and I expect the type system will get better, but I share these frustrations.
> danger abstraction too high, big brain type system code become astral projection of platonic generic turing model of computation into code base. grug confused and agree some level very elegant but also very hard do anything like record number of club inventory for Grug Inc. task at hand
[Edit] it also handled things like parallel resistances, etc.
It was in C++ if I recall correctly.
This is a great idea, that I've haven't had cause to use yet.
[1] https://learn.microsoft.com/en-us/dotnet/fsharp/language-ref...
> "A NASA review board found that the problem was in the software controlling the orbiter's thrusters. The software calculated the force the thrusters needed to exert in pounds of force. A separate piece of software took in the data assuming it was in the metric unit: newtons.... Propulsion engineers, like those at Lockheed Martin who built the craft, typically express force in pounds, but it was standard practice to convert to newtons for space missions. One pound of force is about 4.45 newtons. Engineers at NASA's Jet Propulsion Lab assumed the conversion had been made, and didn't check."
https://www.wired.com/2010/11/1110mars-climate-observer-repo...
There could be issues with memory use, but it could also be implemented as an API, i.e. ensuring values exported from one software package to another were of the correct type, but then store them internally as simple types... Switching everything to a unified metric system would make more sense in the long run, however.
(Edit, that's not possible in c++)
Mind expanding on that? Because what that readme there shows is absolutely possible in C++, I have used a similar system for dealing with natural units.
This is a fundamental limitation of types - they tend to scale poorly to very complex non-uniform structures. That's not to say that they shouldn't be used when they do scale nicely, though!
Is this really a common occurrence? In most situations I've come across, it's the matrix itself (rows/columns) that has a unit of measurement, not the invididual columns. Tensors, rotation matrices, lighting maps: they all use the same units of measurement.
Edit: I should note this is coming from an outside observer as my most favorite languages are dynamically typed like Python and Lisp. But it should also be noted that I like writing in small code bases. Larger ones tend to need typing.
What it will not do is reject the program unless it can confirm that every expression will not cause a type error.
(defun foo () (* 1 "aoeu"))
In SBCL gives me this: ; in: DEFUN FOO
; (* 1 "aoeu")
;
; caught WARNING:
; Constant "aoeu" conflicts with its asserted type NUMBER.
; See also:
; The SBCL Manual, Node "Handling of Types"
;
; compilation unit finished
; caught 1 WARNING condition
But in CCL it gives me no warnings at all and only triggers an error at runtime.Honest question, what should multiplying $100 with $20 give?
Hiring a better engineer for more money may create an efficiency improvement of 1 widget per dollar, with an outlay of $1k extra for the better engineer, giving a total gain of 0.001 widgets per square dollar; hiring a worse engineer for much cheaper may represent an improvement of 0.1 widgets per dollar, at an outlay of $1, giving 0.1 widgets per square dollar.
Perhaps not the most intuitive unit, but it's not impossible. (Though since you can even measure it in dollar-sterling if you like, I suppose that doesn't make it a counterexample to "stop multiplying dollars by sterling".)
You have provided a real example, which I was looking for, of why one might need to express a square dollar; thanks.
I wonder if the people who want to argue "types save you from bugs" see your example as very unwelcome, since they'd want to use "squared dollars" as an example of something nonsensical that should be flagged as a type error. I hope those people can reflect rationally on the limits of type systems in the real world.
In my experience trying to prevent accidental mistakes is a waste of time and often makes our lives miserable. Catching the rare bug by doing complicated work in the type system when it would have been easy to find in normal code anyway is not worth it.
For example, if you have a random variable that's in dollars, its variance would have units of dollars squared. People consider the variance of dollar estimates all the time.
Sorry that the general sentiment is "everything old gets new again". I didn't try to rehash some old news again. I basically blog about things that come up in my daily work life and this topic was something that I felt quite passionately about. From my own experience I felt that type systems, especially in modern languages, are not nearly as well utilised as they could be. Of course there is always a balance to strike, especially with over engineering and needless optimisations, but that is a topic for another blog post another day.
There are almost 8 billion people on this planet; most claims echo prior statements to some degree.
I found your article practical, short, and largely accurate; which is to say: I liked it. I think it could be improved with either an edit or followup which links to similarly-inclined articles, papers, or talks that discuss the topic, so folks can deepen their understanding of the role of type systems in day-to-day programming and PLT.
So we have a type for "country of origin". And then some country that you have in the records splits up into 2 countries, what do you do then? Do you keep a list of all countries that ever existed and keep it up to date?
This approach works good in some cases, but not always
There's a dozen approaches for it, many from the functional programming paradigm. But an example of one approach would be that your consumer functions become responsible for interrogating the data they'll act on -- through assertions or other verification means.
Comparing to the real world: my metal foundry doesn't yell if it gets a non-metal Type of material. But, the logical process it follows (heating to 1000+ *C) takes care of all but a handful of corner cases when you give my foundry the wrong data type.
The choice to wrap all the logic into a "Type" and then get mad when the model logic exists elsewhere is just a choice. And a weird one people get VERY OPINIONATED about.
You can have your name change as well, or your calendar can change, or etc.
Does that mean we stringly type everything? No. You model changes either via a separate field(s) or some kind of change table.
What do you mean by that?
Having a type for "country of origin" would mean that the type gives you limits on what values it can hold (any country known to ever exist) so you can not say something like:
Country c = "Foo"
because Foo is not a country.
I can't imagine having a type for a persons name that holds checks anything but perhaps a strings length, certainly not a list of all possible names.
The calendar example I don't get. We already have "date" types in almost all languages so that "works", although it can be used as example of how hard is to implement some types.
-----
So I say: >> This approach works good in some cases, but not always
And you say: > Does that mean we stringly type everything?
Come on now. "Not always" does not mean "Never"
I agree: All models are wrong; some are useful.
A string is a wrong model for an email address. But it's a pretty useful one.
A custom type sitting lonely in an isolated codebase IS ALSO A WRONG MODEL. Arguably, it might be more a useful one than a string. But that's debatable.
And on that debate, I'll argue a string is a better model because it is a better UNDERSTOOD model by more PEOPLE than whatever MyEmailClassForThisProject you just came up with.
Do you keep a list of all countries
Why would you assume that once you create a type "country", it must explicitly enumerate all possible countries?
This difference between class and type can even be seen in some of those languages. For example, the type *X has no corresponding class in C++ for any X. In Java, the type int doesn't have a class, and the types ArrayList<Integer> and ArrayList<Object> have the same class.
Using classes also leaves entire segments of type-expression on the table or makes them unwieldy e.g. https://en.wikipedia.org/wiki/Tagged_union
So it's orthogonal, classes are just one possible implementation of types.
this is a classic case of not needing more types but needing proper names. Types as concretions, i.e. simply collections of data or functions are a terrible idea because they're static and don't accrete. Data in the real world always does. This becomes very obvious when you go down a paragraph and you see the conundrum:
>For example, let's have a second type called VerifiedEmailAddress. If you wish it can even inherit from an EmailAddress. I don't care, but ensure that there is only one place in the code which can yield a new instance of VerifiedEmailAddress
okay, and for the next email setup let's have a third type, and a fourth type, and a fifth type, and so on. The end result of this is a zoo of types that help nobody to understand anything. It reminds me of an older Rich Hickey talk. When you program a delivery truck you don't make a type for each different truck because of the contents of the truck, you just take your delivery out of the truck and you don't care about the rest.
Does having an EmailAddress type guarantee you won't accidentally accept crap? No, but when you get it wrong, you edit the validation in one place in the system.
Those are types.
Sorry, I accidentally took the delivery out of the email. You made them both have a deliver_to(address) method, you spent most of your comment talking about emails and the computer surely didn't stop my underslept human self from confusing an email address from a physical one.
Of course having ability to do strict typing is a good way to avoid bugs and write reliable code. This is not news.
PROFESSOR: Well, you see, there are different objects, like strings and numbers, that are shaped differently, so we put them into different categories. Those are types! See how simple this is?
<a whole bunch of stuff later>
PROFESSOR: Now the endofunctor of the covariant types are jointly distributed under the free monoid, provided that the pullback doesn't reverberate the time fibration of the dependent type space.
It would be nice to have stuff for these in the standard libraries but currencies are a moving target and needs constant update to handle the quirks of the real world. For the same reason it's extremely hard to create an "Address" class that handles every possible scenario.
In my book the string is a great way to store a country of origin because everything else depends on the usage of that information.
Yes but they should also be compositional, one should build larger types from smaller types. This is how we scale to solving difficult problems.
> but currencies are a moving target and needs constant update to handle the quirks of the real world.
This is a separate issue orthogonal to types. The problem of modelling the real world and what models might be more generally useful. No model is perfect, all have trade-offs.
> In my book the string is a great way to store a country of origin
As an implementation using a string sounds pragmatic, but that should not be its type! You should still create a currency type and parse into it. Then you won't accidentally pass an arbitrary string to a parameter that is supposed to be a currency.
A type (of a term) represents the set of all possible values for a particular term. An "object" in OOP, does not have any formal definition, but is typically a first-class module with mutable state. As such, they can be represented by a term and therefore can have a type.
I can see a type system designed from the ground up to do something like this. But as a bolt on to existing languages it's pretty clunky.
type VerifiedEmailOptedOutOfMarketing = { email: string; is_verified: true; is_opted_out: true; };One type is probably going to align with storage and transport better, and you probably mostly want to treat verified and unverified email addresses the same except for some very specific situations. (E.g., maybe only your EmailBlaster cares, where it's like a privilege: some can send to unverified emails and some can't.)
This seems like a bad for types to me.
(It's all code you write and data you design -- types are just one tool... you need to think about they best tool, not get fixated on one, no matter nice it is.)
recognize : String -> UnverifiedEmail
validate : UnverifiedEmail -> VerifiedEmail
send : (VerifiedEmail, Message) -> ()
You can then use visibility controls to universally guarantee that recognize and validate must be called before send. No test can ensure this is true.Under the presumption that send should only perform work on verified emails, the alternative is not being able to be confident that emails passed to send are pre-verified. This means that send must check this flag, and therefore have the ability to fail due to non-verification.
This isn't inherently a problem, but it can lead to a failure to separate concerns. If one part of your system is responsible for parsing and validation and a separate part responsible for interacting with the sending machinery, it's unfortunate if the latter part can fail due to a failure to verify the email. These systems have now implicitly shared responsibility.
You can try to guarantee that no email is passed from the first system to the second without being verified, but this can be challenging. It's a universal property. Tests can show the presence but not the absence of bugs.
But those types we showed at the beginning provide exactly that guarantee.
> maybe only your EmailBlaster cares, where it's like a privilege: some can send to unverified emails and some can't
A boolean flag is strictly inferior, because it is a runtime check. You can only ever be sure that you're processing verified emails at runtime, and there's no way to require that the code guards against that in all places. If they're different types, you can't even pass an unverified email to code that needs verified emails, so you eliminate the entire possibility at compile-time.
>verified and unverified email addresses the same except for some very specific situations.
This is when typeclasses are a useful concept (or interfaces). Instead of designing your function around a concrete type, you can codify that the caller needs to provide any types that satisfy some requirements. For example, if the function only cares that the input can be treated as a string, you can ask for something like `As<String>`. Then, the caller can provide literally anything as long it implements `As<String>`.
> types are just one tool
Indeed, types are just a tool. But it is a much better tool. Its an electronic shaver instead of rusty axe. This is my biggest gripe with "simple and small languages". More often than not, you just end up writing more verbose and complicated code just to compensate.
To illustrate, I wanted to implement `INCR KEY VALUE` from Redis.
```
item, exists := db.keys[key]
if !exists { return }
value, ok := item.Value().(string)
if !ok { return }
intValue, err := strconv.ParseInt(value, 10, 64)
if err != nil { return }
intValue++ // <== this is literally the only meaningful work I am doing
db.keys[key] = intValue;
```
Wow, lots of great, thoughtful replies, thanks!
But I'm not convinced...
Maybe it's just a bad example (this is what the main article is about, though)... Generally speaking, you need to be able to send emails to both verified and unverified emails. The difference is in what the email you are sending is about. That's why VerifiedEmail as a type doesn't make a lot of sense to me.
You'll need sendToUnverifiedEmail(email: UnverifiedEmail) and sendToVerifiedEmail(email: VerifiedEmail), and have code to get the right type to pass to the right function the in the right circumstance...
You've got the same potential for getting this stuff wrong, whether you express it in a type or in imperative code or however you express it.
Replies are generally assuming the type part of the code is bug-free and the imperative part of the code is not, which just isn't reasonable.
Also, the static vs. runtime stuff is irrelevant to this example: the verified status of an email address is a runtime property that cannot be known at compile time (OK, I'm assuming you don't hard-code verified email addresses). I.e., due to a bug, a value of type VerifiedEmail could be created for an email address that is not really verified. Then, your static checks for VerifiedEmail don't help you at all.
Further, "verified" vs. "unverified" is really a business concern around when it's OK to send certain kinds of emails to the address. It has a fairly standard definition, but there are qualifiers for email addresses that are at least as important to a business that aren't. E.g, did the owner of the email address opt in to marketing emails? Or opt out? Or did not express a preference (yet)? Are you going to have types for those? You'd have to have 3 X 2 types to encode that... that's (verified, unverified) X (opted-in, opted-out, didn't specify) types. Then your business adds a new kind of email, securityAlerts, so now you've got (verified, unverified) X (opted-in, opted-out, didn't specify) X (gets-security-alerts, doesnt-get-security-alerts). Oh wait, some emails shouldn't go to banned people. So now you've got: (verified, unverified) X (opted-in, opted-out, didn't specify) X (gets-security-alerts, doesnt-get-security-alerts) X (banned, not-banned). And you need a "send" for each type, called at the right spot.
So...
Are the types really helping here?
Only if you're using a language with an insufficiently strong type system (e.g. Java, C#)
in typescript:
type UnverifiedEmail = { address: string, verified; false }
type VerifiedEmail = { address: string, verified; true }
...
type Email = UnverifiedEmail | VerifiedEmail | FooBarBazEmail
const sendToEmail = (email: Email): Promise<void> = ...
in Haskell: class SendTo t where
sendTo :: t -> IO ()
newtype Email = Email string
instance SendTo Email where
sendTo (Email address) = ...
newtype VerifiedEmail = VerifiedEmail Email deriving (SendTo)
newtype FooBarBazEmail = FooBarBazEmail Email deriving (SendTo)
> I.e., due to a bug, a value of type VerifiedEmail could be created for an email address that is not really verified. Then, your static checks for VerifiedEmail don't help you at all.Of course they do - they tell you that the bug is in the verification code, and not in any of the thousands of lines of business logic separating it from the place where the error was found.
You obviously have no idea what a type is.
type VerifiedEmail = { email: string; is_verified: true; };
type UnverifiedEmail = { email: string; is_verified: false; };Ah, you reposted this, hoping the weekend crowd might be kinder. Cheeky lad.
In terms of ease-of-debugging, sure, splattering local variables and extra control statements may allow you to break/inspect a certain class of bug in a certain way. But it also creates a lot of noise and makes the code a lot more "dense". It's hard to see given an example in isolation, but when all of your code looks like this it can make it significantly more "tiring" to understand. "Easy to debug", while important, is also something that must be balanced against other factors.
And in terms of easy-to-understand, again, I agree that the author's example has a slight edge (give the first one a shot though... it's not so bad). But what does it mean for a `Contact` to both be "inactive" and also "a family or friend"? They have forgotten to capture the single most important condition! Similar to my first point, it can be hard to see the issue when given an example in isolation, but imagine looking for whatever condition or rule the author is enforcing in a sea of other blocks that look similar.
A simple comment over the original version would suffice for me:
// If the contact is staleSure, I'm on board: I also want an e-mail address type. Just not in your shit language in which something can be of type String, yet be null reference.
void fun(string x) // std::string
{
}
x cannot be null. The reference semantics (like a copy of a string sharing the data with the original) is an implementation detail/optimization encapsulated inside what appears to be a value type.The tools are there in C++ to create ideal types for your problem domain which just look like value types that have no null domain value (or any other value you don't want), and standard strings are like this.
But in Kotlin non-nullable types are the default. In C++ references are non-nullable. Both include nullable types too.
In that specific example, String can't be null in Kotlin and neither can string in C++, although you can make them nullable by using e.g. a pointer or an optional type.
Having everything be nullable like in Java is unarguably 100% a mistake.
Dynamic typing allows you to focus on what really matters, not trivial business logic OOP hierarchies that get inevitably ignored.
I feel like in app development, there’s something honest about dynamic typing. You’re focusing on the instance rather than the unnecessary model definition that again, nobody uses and redefined elsewhere anyway.
Tbh, I’ve never used a language like JS professionally. I’m sure a lot of code bases are copied and pasted messes with state dependencies and things that make it so you HAVE to focus on the type. It’s an app language, you’re just not gonna find elegance lol.
I’ve been a C# dev for a while now. I’ve just found that outside of libraries/frameworks/etc how rarely object model code actually gets reused. And I appreciate the cut to the chase aspect of that.
Insert quote about gorilla with banana in forest.
And the richest and most expressive type systems are arguably specifically non-OO. Nor are OO languages necessarily statically typed (Smalltalk, Self, Python, Ruby, Javascript, ...)
The reverse is true, you can have static type-checking without OOP, like C, Rust, Haskell.
I can argue whatever type you use in place of a string will similarly be "not a great type". This can be argued in perpetuity because no type/map actually matches the reality it's encapsulating.
Type systems require you to build a Pretty Good Theory of your problem space so that your types can overlap with reality/actual usage as much as possible.
The problem is at planning/design time, you'll have a Pretty Crappy Theory of your problem space and can only get a Pretty Good one after having wrestled with it for a while.
A dynamic, more forgiving language, allows you to build what you can today with the theory you have, and then change it in the future when your theory gets disproven.
All Types are wrong, but some are useful.
Will Money type solve all problems? Is it better than decimal, from POV of prevention of mistakes - yes.
CInches in1, in2, in3; // basically just floating points
CCm cm1; // basically just floating points
in1 = in2 + in3; // no compilation error
in1 = in2 + cm1; // compilation error
in1 = in2 * in3; // compilation error
in1 = 2. * in2; // no compilation error
float foo(CInches *);
foo(cm1); // compilation error
Obviously there's a lot of code behind "CInches", but it catches so many errors.
Which other languages also support this?Haskell and related languages have distinct types; so does Nim. But even if there's no direct language support, you can always emulate them by having a compound type (record, struct, class, whatever you call it) with one field.
A string is some bytes in a line with a terminator at the end.
An integer is a group of signed or unsigned bytes.
An enum value points at another value with a pointer.
Etc.
What I think this describes is some validation classes, which don’t need to be built into a language’s runtime. Primitive types have a real reason for existing when compiling these apps, a validation class doesn’t. It can just be a library, in which case this is a nice API for validation!
Instead, it's more normal to think of types (primitive or not) as descriptions of what can be done with a particular kind of value - from this point of view, it's obvious why long* is a different type than long (you can dereference it) or int(*)(int) (you can't call it). With this new definition, we can also see why we may want to distinguish EmailAddress from String - you can send an email to an EmailAddress, but you can't send an email to a String; conversely, you can sort the characters of a String, but you can't sort the characters of an EmailAddress.
OOP, where it means anything at all, involves runtime selection of operations according to types organized in hierarchies.
TFA is about compile-time type compatibility enforcement.
I come from the C++ world, where it is natural that types are checked at compile time. And where you would use classes to implement the 'sophisticated types' the author suggested.
So in the ears of a C++ programmer the article says: design your classes well.
It replaces clear statements like "int x = 0;"
With "var x = 0;" as if that's somehow better. So instead of having clear blocks of types that you can visibly read, it's concealed. And the type could change each time you run the compiler.
What many people didn't realize was that C++ was hard DESPITE the type system, not because of it. This was soon rectified with type script which eventually caused a complete flip of opinion in the industry once javascript developers realized how much better it is.
The other question to this equation is why was python so easy to program for DESPITE not having a type checker (it has external type checks now, but I'm saying before this)?
The answer is deterministic errors and easy traceability. If you have an error that happens either at runtime or at compile time you want to easily know what the error is, where it came from, and why it occurred. Python makes it VERY easy to do this. Not all type checkers make this easy (see C++).
In actuality type checking is sort of sugar on top of it all imo. Rust is great. But really the key factor to make programming more productive is traceability. Type checking, while good is not the key factor here.
Think about it. Whether the error occurs at runtime or compile time is besides the point. Compile time adds a bit of additional safety, but really if an error exists, it will usually trigger at some point anyways.
The thing that is important is that when this error occurs whether compile time or runtime you need as much information about it as possible. That is the key differentiator.
That is why typeless python and typed rust, despite being opposites, are relatively easy to write complex code for when compared to something like C++.
Well, if the error is at compile time, there's no chance that code makes it to production and affects customers.
If the error is at runtime, you need to have tested that edge case and if you haven't, there could be customer impact.
I mean, once you see a few TypeErrors in Python code with no type annotations, or a few NullPointerExceptions in Java where there's no compile time null checking by default, I think it becomes very clear that catching things at compile time is much better...
Let me put it this way. If you have type checking I'm saying that from my anecdotal experience you probably catch 10% more errors then you would normally catch before deployment. The reason is you're bound to run runtime tests anyway and these tests cause you to correct all your little type bugs anyway.
And this isn't even errors you would'nt've caught. It's just about catching the errors earlier.
That's it. Catching 10% of errors after deployment rather then before... that is not a huge deal breaker. Type checking benefits are marginal in this sense. Yes agreed it's better, but it's not the deal breaker.
I'm trying to point out the deal breaker feature. The delta difference that causes python to be BETTER then C++ in terms of usability and safety. Type checking is a negligible factor in that delta is basically my thesis. This is subtle. A lot of people are going on tangents but that is my point.
Types are exactly equally as "traceable" in C++ as in Rust.
Read what I wrote carefully. I'm not talking about type errors. I'm talking about errors in GENERAL.
Your comment is carefully tailored to incite flame war. You represent HN bias at it's finest, reading something and giving a casual dismissal without really interpreting it.