13 ms·
Why don't more languages offer flow typing?
- joe_fishfish 5y agoKotlin does this, it's great.
- irrational 5y agoI forgot about flow typing. Is that still an option on iOS?
- rootusrootus 5y agoYou're thinking of swype, or whatever iOS calls it. Yes, it is built in to iOS. This 'flow typing' is a programming topic, nothing to do with mobile keyboards.
- jerf 5y agoThis is not a complete answer, but covers some languages. If you program too exclusively in dynamically-typed languages, you can be too used to not thinking about how physically large your types are, because you work in a world where everything is boxed, and allocations so plentiful you don't even hardly have a way of thinking about them because your language does them at the drop of a hat, and so on. But there are many strongly-typed languages that look at types at compile time and decide how large they are, by which I mean, how many bytes of physical RAM they take. These languages have gotten better and better over time at the APIs they offer that give the benefits of this without incurring the programmer costs, but under the hood, no matter how slick they've gotten, there is still a mapping of type -> size & layout. In these languages, this sort of narrowing is a great deal more complicated, because it implies a new type, which will probably require more allocation. For instance, in the example given between a "string" and a "number", that definitely looks like two differently-sized types to me. This would become very complicated very quickly as you start having to allocate these things in ways that can't be transparent to the user, you have to pass these types around, etc. etc. One would expect that the affordances of statically-typed languages would end up being very different. And in fact, they are. Many modern statically-typed languages can solve the same problems outlined in the post, they just solve it in a different way. In this case, an Either, or more generally, a sum type. Then "getViewsOf" returns a value of that sum type and you can switch on it based on the value. It isn't exactly the same, in fact I'm not sure there's one aspect of it that's the same let alone the whole, but it solves the same problems. So my suggestion would be that there are in fact a lot of languages that essentially have the same feature. They just don't call it "flow typing" and it's not spelled the same way.
- jamil7 5y agoYeah, I hadn't heard the term "flow typing" before but I thought the same thing, in other languages I've used you'd use a Result type and a switch to do what the author was doing.
- xg15 5y agoIsn't this sort of an orthogonal problem? Flow typing implies that your type is in some way unknown at the time the code fragment is evaluated. I think this can mostly happen in two situations: 1) The type is generic: A function may be called with a different type on each call site - but for each particular call site, the type is known at compile time. 2) The type is polymorphic - i.e. the full type is not known at compile time at all. When the first case occurs, a compiler could either go the C++ way and generate different, non-generic variants of the function - or go the Java way and just pretend the type is polymorphic. For the "C++" option, I think flow typing would fit very well without introducing additional complexity: For each variant the compiler generates, it knows which concrete type is used, so it can evaluate the condition at compile-time and just drop the branches that don't apply for a particular variant of the function. For polymorphic types, on the other hand, the additional complexity is already priced in: You need some kind of mechanism to represent values of different structure anyway (pointers, unions, whatever) - so flow typing wouldn't be much more than some syntactic sugar to save you from writing casts you'd have to do anyway.
- simulate-me 5y agoAnother very common case in TypeScript (and other languages) is union types. Other languages have constructs for checking the underlying type of a union type directly, but typescript is smart enough to figure it out based off of the properties of the constituent types and what you've checked so far in your code. e.g. interface A { type: 'a' } interface B { type: 'b' } type C = A | B; const c: C = ...; if (c.type === 'a') { /* c is of type A here */ }
- jakelazaroff 5y agoThat's basically case 1, right? The compiler knows which type is being used at each call site, so it can generate a separate function for each type in the union and eliminate the type check/dead branches. I guess the exception would be if you have a non-homogenous array that you try to map over. In that case there's probably no way around boxing the values.
- endtime 5y agoDart does this, or at least something very similar, and it is really nice. It's most useful for narrowing nullable types to their non-nullable counterparts after a null-check, but the subclass case works too.
- scns 5y agoAs your sibling allready stated, in Kotlin it is known as smart casting. Good to see that the article allready recognizes the prior art from ML which was conceived in 1971.
- jimbob45 5y agoDart’s “..” operator, which allows void-returned function calls to be chained, adds no performance cost to the code and is one of my favorite pieces of Dart.
- mosdl 5y agofyi Java 16 did add pattern matching for instance of so you can avoid the cast.
- timmg 5y agoI was going to point this out, too. I thought it also supported cases like: if (foo instanceof Bar) { // foo is typed as Bar in this block } Is that true, or did I get that wrong?
- qw 5y agoif (o instanceof String s) { var foo = s.indexOf("bar"); } Java 17 introduced a preview of the same feature for switch statements return switch (o) { case Integer i -> String.format("int %d", i); case Long l -> String.format("long %d", l); case Double d -> String.format("double %f", d); case String s -> String.format("String %s", s); default -> o.toString(); }; Future versions will have improved support: http://openjdk.java.net/jeps/420 http://openjdk.java.net/jeps/420)
- MH15 5y agoI use the `o instanceof Class c` thing all the time in my hobby compiler on Java 16 right now. The Java 17 version will greatly simplify much of my codebase.
- throwaway81523 5y agoIs this something like GADT's?
- littleroot 5y agoFeels like type refinement via pattern matching on GADTs is more expressive than this
- valenterry 5y agoThe answer is simple: because other languages don't need it - they have different features to deal with it. The author even mentions it: pattern matching. Just that he picks a language that doesn't support union-types. But that doesn't mean that flow typing would be necessary here - it means that the language(s) should support union-types and extend their pattern matching accordingly. In fact, I would say that flow typing is almost like a workaround for missing pattern matching.
- lambdasquirrel 5y agoYeah, the same code would look much clearer in a language with union types. Heck even Swift cribbed them. switch resp { case Result(val, meta): doStuff(val) case Error(msg, code): logger.main.debug(msg) }
- dragonwriter 5y agoYou mean sum types, not union types (TypeScript has union types, your example uses sum types.)
- marcosdumay 5y agoAn union type is the C attempt to implement something like sum types, that other languages extend a bit to literally implement sum types. Those two words live on different contexts, as unions are about a memory usage pattern, and sum types are about conceptual software design, but they often go together on the same language feature.
- munificent 5y ago> An union type is the C attempt to implement something like sum types Sum types, unions (in the C sense), and union types are three distinct concepts. Wikipedia's article confusingly merges the latter two concepts together but they're different thing. TypeScript[1] and Scala[2] have union types, but they have nothing like C's notion of "unsafely reuse the same bits in memory to be interpreted as different things". [1]: https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types https://www.typescriptlang.org/docs/handbook/2/everyday-type... [2]: https://dotty.epfl.ch/docs/reference/new-types/union-types.html https://dotty.epfl.ch/docs/reference/new-types/union-types.h...
- john567 5y agoI'm trying to design a programming language that can be compiled into C99 and it's not at all clear to me how you represent these things statically. The idea from a programming point of view is really nice and in a dynamic programming language environment everything is an object and you can pass whatever through any function. This doesn't work all that well if you want to translate your program into a statically typed environment, in a cost effective way. You need some way to reason about the type information that hopefully doesn't create memory allocations all over the place. This can be done by introducing tagged data types but it can also lead to a combinatorial nightmare where you need to generate a lot of extra code. In general, I don't think all this complexity is warranted in a statically typed environment and depending what you are doing this doesn't end up being that important. Even if, it's a nice to have, it's mostly that, a nice to have.
- duped 5y agoTypically you wouldn't change the compiled code too much, it changes what is valid to type check and inserts casts as necessary. In general, flow typing just makes more programs type check.
- john567 5y agoYou have to emit the plumbing for something like flow typing to work. You can't omit the type checking because you cannot account for all the possible call sites at compile time, that will lead to a combinatorial explosion. You do end up generating code to pass arguments with temporary type information. Take the example of a variant type, which is what I'm most interested in. You have something like type X = int32 | string | boolean | SomeCustomType When you then write a function that accepts X as a type you need to be able to pass any of these things into that function. To do that you need a auto user defined type that can carry the information, in C that might look like this struct { union { int a; char* b; boolean c; } data; Tag type, } then your type assertions can work on this. If you wrote this in my fancy language: if x != nil { // x cannot be nil here x.foo } the equivalent C code for this would be something like void f(X x) { if (x.type != WELL_KNOWN_NIL) { x.data.foo // we know it's safe to access foo (assuming we typed X as Foo | nil) } } Trying to solve this at compile time without any runtime checks might be possible but I don't know that it would be better, it might be too slow if I have to solve some nasty combinatorial problems. This is at least somewhat easy to explain.
- phillipcarter 5y agoIt's not really called out as such, but the C# 8 feature Nullable Reference Types is an example of deeply embedding flow typing into a language. The nullability state of a reference type can change based on many things you can do in code. It's really neat and extremely intricate.
- SideburnsOfDoom 5y agoIt's exactly that. e.g. inside "if (x != null) { }", or after "x = y ?? throw new ArgumentNullException()" then x is known not null. C# has flow types, but only in this specific and limited way. You could say that about a lot of programming language features.
- fuzzy2 5y agoThis is called Type Narrowing by the way. Control flow analysis is only one way this works in TypeScript. Most languages don’t have the type system necessary to make this work in a sensible way.
- jillesvangurp 5y agoIn Kotlin this is called smart casts. It works great. Doing a null check means that after that you can treat the variable as non nullable. Doing a type check, narrows down the type to what you just checked. Or going down a switch statement branch on the type actually implicitly does the cast as well. It's both strongly typed and convenient. Kotlkin even goes a step further and introduced contracts few versions ago that you can bind to functions. For example calling isNullOrBlank() on a nullable string changes to the type to nullable if the answer is false. You can write your own contracts for your own functions even. This is what the source code for this function looks like: @kotlin.internal.InlineOnly public inline fun CharSequence?.isNullOrEmpty(): Boolean { contract { returns(false) implies (this@isNullOrEmpty != null) } return this == null || this.length == 0 } I guess typescript does something similarish.
- LordDragonfang 5y ago>For example calling isNullOrBlank() on a nullable string changes to the type to nullable if the answer is false For anyone confused like I was, that's a typo, it changes the type to not nullable. Likewise, isNullOrBlank() is an extension of the listed isNullOrEmpty(). https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/is-null-or-blank.html https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/is-... https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/is-null-or-empty.html https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/is-...
- mhink 5y agoYeah, they're called "type predicates" (or "type guards"). So you can do something like function isFish(pet: Fish | Bird): pet is Fish { return (pet as Fish).swim !== undefined; } and Typescript is smart enough to let you use it in an if-branch // Both calls to 'swim' and 'fly' are now okay. let pet = getSmallPet(); if (isFish(pet)) { pet.swim(); } else { pet.fly(); } Typescript also has typechecker support for assertion functions. So like, function assertIsDefined<T>(val: T): asserts val is NonNullable<T> { if (val === undefined || val === null) { throw new AssertionError(`Expected 'val' to be defined, but received ${val}`); } } Which we can use like so: function doSomething(pet: Fish | undefined) { assertIsDefined(pet); // safe to use without checking for null/undefined, // because our assertion function narrowed the type for // usage later in the function pet.swim(); }
- colonwqbang 5y agoDoesn't "flow typing" seem more like a bandage for the if-statement in languages that have nothing better. There is a language construct which is purpose built for unpacking sum types: The case expression (sometimes called match expression) case resp of Views n -> ... Error e -> ... Inside each branch, we have access to a value of the more specific type.
- yxhuvud 5y agoWhy should type narrowing be limited to just case, and not both case and if? I'd argue that both of these are examples of flow typing.
- colonwqbang 5y agoMy point was that if your language has a dedicated facility for case analysis, you don't really need to use if statements for that purpose. You just use case. That's one answer to "why don't more languages have flow typing".
- ljloisnflwef 5y agoYes and no. Consider predicate types, or first-class null safety through unions. If statements introduce propositions, which type systems could take advantage. example: let idx = randomInt(); if (0 <= idx && idx < arr.length) { // proposition introduced // idx now has type: int where 0 <= idx && idx < arr.length // irrefutable proof of safe indexing by type propositions let value = arr[idx]; }
- chriswarbo 5y agoNobody's mentioned dependent typing yet, but languages like Idris and Agda will "narrow types based on control flow" (in fact, since they're pure-functional, control-flow is the same as data-flow). For example, zipping two vectors of the same length: zip: Vec n t1 -> Vec n t2 -> Vec n (t1, t2) zip Nil Nil = Nil zip (Cons x xs) (Cons y ys) = Cons (x, y) (zip xs ys) Here the 'Vec' type has two constructors, Nil and Cons. Since their types specify the same length, the type-checker knows we can ignore the 'zip Nil Cons' or 'zip Cons Nil' cases.
- alduin32 5y agoThis is amazing, but I don't understand the justification : > Since their types specify the same length, the type-checker knows we can ignore the 'zip Nil Cons' or 'zip Cons Nil' cases. Specifically, I have trouble understanding "their types specify the same length". Whose types ?
- masklinn 5y agoThe types of the vectors: the length is a generic parameter to the type. So a Vec 4 Int and a Vec 5 Int are different types.
- alduin32 5y agoOh right, the output is parametrized by the input. I suppose this is what dependent types are. That's really awesome. Thanks for the explanation.
- masklinn 5y ago> Oh right, the output is parametrized by the input. I suppose this is what dependent types are. Dependent types is really about the ability to parameterise types over values, and reason on those values-dependent types.
- earleybird 5y agoYou might read the Vector type constructor 'Vector N T' as "this is a Vector of T's of length N'.
- cmrdporcupine 5y agoSo I've been working on a personal project which is in a mix of C++ and TypeScript -- partially because of practicality and partially to learn TypeScript, and I'm really liking the language. One question I have is whether there's any possibility that TypeScript could, in the long run, gain performance advantages over pure JS? That the compiler could leave behind some type information artifacts so that V8 (or similar) could use that information at runtime to optimize method dispatch and other operations? Likewise with the flow pieces mentioned here, I gotta think that the VM could optimize out certain runtime checks on dispatch or conditional branching if it knows that the compiler has already checked for these?
- noveltyaccount 5y agoI really enjoy Typescript, but every time I use it I think to myself, this would be so much better if were built on top of something other than JavaScript. Maybe AssemblyScript or some other TS to WASM target will come along with a better underlying type system and a good standard library. Dare to dream.
- cmrdporcupine 5y agoYep. Except that the base semantics of the language really are tuned for JavaScript. So I can't see a separation being feasible between the two. It would have to be a new language, sharing maybe 90% of syntax etc. but differing in some places where interoperability with JS has forced compromises (base number types is one thing I can think of. I'd like separate int and floats, etc.) and, yeah, targeting WASM etc. And honestly, I'd be game for that. Esp if it came with performance advantages.
- noveltyaccount 5y agoYes that's what I meant by proper type system - int, float, decimal... proper date/time/timezone support. To name a few :)
- munificent 5y agoWhat you're describing is essentially the origin story for Dart.
- sfvisser 5y agoContrary to what many comments here state, I don’t think this is only useful in a dynamic runtime kind-of environment. I often think I could use some form of this in Rust and/or Haskell. In a very specific way: I often want a single constructor/branch of an enum (sum type) to a be a type as well, specifically a sub-type of the full enum. So once I learn about what branch a value is, I can treat it like that and even first class pass it around – without unpacking. I think this should be implementable in Rust without any runtime overhead as pure syntactic sugar. You get the same effect by creating a new struct for every set of values per branch and using those wrappers instead.
- IshKebab 5y agoYeah this would definitely be useful in Rust. It isn't in Rust currently because enum variants aren't distinct types, but that's definitely on the to-do list. I suspect Rust will get this eventually.
- marcosdumay 5y agoYou want something like Haskell's Typeable class?
- chowells 5y agoYou can do this in Haskell with GADTs. data Up data Down data Foo tag a where Up :: a -> Foo Up a Down :: String -> Int -> Foo Down a When you enable all the necessary extensions for this to compile, it gives you exactly what you've asked for. If you leave the tag variable polymorphic in a function argument, you can receive values of either constructor. If you specify Foo Up a, you can only receive values with the Up constructor. It might be a bit more verbose than you want, with needing to declare types to use as the type level tags. (I looked into using PolyKinds and DataKinds to use the constructor as its own type argument, but GHC doesn't allow that type of recursion.) But it does do exactly what you've requested - it allows you to treat each constructor as a separate type in some contexts, and allow any of them in other contexts.
- sfvisser 5y ago
- baby 5y agoWhat’s the difference with type inference? Is it that flow typing can happen at runtime for just in time /noncompiled languages?
- yxhuvud 5y agoFlow typing is basically a kind of type inference where the derived type of a variable is allowed to be different in different places of the code, based on control flow. However, not all type inference is flow typing. Flow typing is definitely a thing also in compiled languages, one example is Crystal, which is both compiled and has flow typing.
- mhh__ 5y agoUnless you build a compiler around it it's probably quite hard to implement
- Orangeair 5y ago> Even in the presence of such tests, Java demands that the author cast their objects appropriately. This is one of the many reasons why I've found Kotlin so refreshing after five years of writing enterprise Java https://kotlinlang.org/docs/typecasts.html#smart-casts https://kotlinlang.org/docs/typecasts.html#smart-casts
- ashton314 5y agoSee also “Occurrence Typing” in Typed Racket. [^1] > One of Typed Racket’s distinguishing type system features is occurrence typing, which allows the type system to ascribe more precise types based on whether a predicate check succeeds or fails. One of the big advantages is that it lets a text editor use the added information to supply better auto-complete suggestions. It also makes it possible to eliminate some safety checks at runtime because the type checker has ensured that certain calls will be safe based on prior conditional checks. [^1]: https://docs.racket-lang.org/ts-guide/occurrence-typing.html https://docs.racket-lang.org/ts-guide/occurrence-typing.html
- adamddev1 5y agoI looove flow typing / type narrowing. HTDP taught how to think through it (data type narrowing / checking for and handling different types) while building functions. TypeScript let's me enforce the same process with intellisense warning me whenever I get off track. It's been a joy to see how quickly and solidly the code stacks up.
- cannabis_sam 5y agoThis question actually has a simple answer: Most languages have embraced statements over expressions for a lot of language constructs. This causes a lot of issues: An if-statement is in essence a function from boolean to unit/() (i.e. from a barely useful type to a type with no useful information), while an if-expressions will contain enough type information to at least provide this kind of “flow typing”, (even if it induces an amount of boolean blindness.) It’s even worse when you get to loop-statements, which often is a function from unit/() to (unit/() | bottom/⊥), compared to a map or a reduce or fold, which again provide enough type information to provide this kind of “flow typing”. Sometimes you will of course need to provide impure conditionals or loops, but that can be made explicit in the type system. IMHO statements in programming languages are an anti-pattern.
- dragonwriter 5y ago> Most languages have embraced statements over expressions for a lot of language constructs. Some of the most used languages with flow typing have statements and follow them for typing, so that is emphatically not the reason.
- cannabis_sam 5y agoMy view might of course be a bit outdated, do you have any examples? (Specifically for this point: “and follow them for typing”)
- cmrdporcupine 5y agoThe example in TFA. TypeScript.
- cannabis_sam 5y agoSo my point stands…?
- munificent 5y agoAlmost all of the languages I know that do "flow typing" (TypeScript, Flow, Hack), "smart casts" (Kotlin), or "type promotion" (Dart) are fairly statement-oriented. Expression-based languages tend to have pattern matching which provides another way to solve the same problem. Flow typing is most useful in imperative languages where code like this is common: if (foo is! Bar) return "not a Bar"; foo.someBarMethod(); Early returns and other imperative control flow is idiomatic and it's annoying if the static type system doesn't understand it. In a more functional or expression-oriented language, early returns and other imperative control flow like that is rarer, so there's less "flowing" to type over.
- munificent 5y agoI work on a language, Dart, which also relies heavily on flow typing (which it calls "type promotion" because apparently every language needs their own name for it). We use it both for subtype tests and null checks. It's really nice and a large net win for Dart especially given its history. However, if I had a time machine and could redesign Dart from scratch, I would be tempted avoid flow typing and instead do something more like Rust and Swift do: Variables keep their static type but have pattern maching and nice syntactic sugar for simple use cases of it to make it easier to bind new variables with the narrowed type. The main problem is that flow typing is very complex, subtle, and can fail in ways that users find surprising. For example: foo(Object obj) { closure() { obj = "not int any more."; } if (obj is int) { print(obj.abs()); } closure(); } This function is technically safe, but it's very hard for static analysis to reliably prove what kinds of flow analysis are valid when closures come into play. If that closure can escape, it can be impossible to prove that it won't be called before the variable is used. A simpler, more annoying example is: class C { Object obj; foo() { if (obj is int) { bar(); print(obj.abs()); } } bar() {} } This code looks like it should be fine. But if C is an unsealed class and some subclass overrides `bar()` to assign to `obj`, then the promotion could fail. Because of this, Dart can't promote fields and it causes no end of user annoyance. Top-level variables and static fields have similar limitations. Even when it works, it can be surprising: foo(Object obj) { if (obj is int) { var elements = [obj]; } } Should `elements` be inferred as a `List<Object>` or `List<int>`? What about here: foo(Object obj) { if (obj is int) { var elements = [obj]; elements.add("not int"); } } Should that `add()` call be OK or an error? Flow analysis is cool and feels like magic. It does the right thing 90+% of the time, but there's a lot going on under the hood that pops up in weird surprising ways sometimes. It's probably the right thing to do if your language has already invested in an imperative style. But if you have the luxury of defining a language from scratch, I think you can get something simpler and more predictable if you make it easier to define new variables of the refined type instead of mutating the type of an existing variable. Dart is heavily imperative, so I think flow analysis makes sense for it. Accommodating an imperative style is one of the main things that makes Dart so easy for new users to pick up, and that's an invaluable property. But I admit I envy Swift and Rust at times.
- kragen 5y agoHafiz writes: > almost every enterprise Java codebase I've had to work with has observed this pattern of testing whether an object is an instance of particular subclass, and using that to drive further computation. Even in the presence of such tests, Java demands that the author cast their objects appropriately. Of course you can always introduce an intermediate variable after the test, but I argue that this is still too much—upon verifying the instance of an object, the type system should be smart enough to update the object's type appropriately! Because Java considers null to inhabit every type, in a Java project a few years ago, I handled this in a dynamic_cast-like way, as follows (abbreviated): public abstract class Security { // ... public Stock asStock() { return null; } public Future asFuture() { return null; } } public class Stock extends Security { // ... public Stock asStock() { return this; } } public class Future extends Security { public final SecurityTable factory; public final String exchange, symbol; // ... public Future asFuture() { return this; } } This allows you to write relatively uncluttered type-dispatching downcasting code like this: public boolean contains(Security sec) { Future f = sec.asFuture(); return f != null && SecurityTable.this == f.factory && f.symbol.equals(symbol); } Of course this violates the open-closed principle (the abstract base class should be closed for modification) and official OO doctrine is that if you want different behavior for different subclasses you should put that behavior into a method that gets overridden by the subclasses, not in a conditional that attempts a downcast, so we did that a lot more often. But I found it a pleasant solution to the problem in the context of this Java project. Of course it doesn't help in languages without implicit nullability, like TypeScript, which would ideally be all statically-typed languages. — ⁂ — The big difficulty with flow typing is, as I see it, not that it clashes with nominal typing; it's that it incorporates your compiler's static control flow analysis capabilities into your language's type system. Consider Hafiz's Java example: if (node instanceof DomNode.Element) { Layout layout = ((DomNode.Element) node).layout; return new RenderNode.Styled(layout, /* ... */); } return new RenderNode.Noop(/* ... */); It is entirely reasonable to request that the type system handle this and allow you to write: if (node instanceof DomNode.Element) { Layout layout = node.layout; return new RenderNode.Styled(layout, /* ... */); } return new RenderNode.Noop(/* ... */); But how about this? Now we're no longer inside the static extent of the `if`, and we're depending on the compiler to recognize the unconditional early return: if (!(node instanceof DomNode.Element)) { return new RenderNode.Noop(/* ... */); } Layout layout = node.layout; return new RenderNode.Styled(layout, /* ... */); How about constant folding and partial evaluation? if (1 == 0 || node instanceof DomNode.Element) { Layout layout = node.layout; return new RenderNode.Styled(layout, /* ... */); } return new RenderNode.Noop(/* ... */); Do we want to skip type checking entirely for dead code? Straightforwardly flow typing gives us that the type of every variable inside unreachable code is void, the uninhabited type, so any operation whatsoever on it is type-valid. Do we want the compiler to accept code like this? if (1 == 0) { Layout layout = node.layout + node / node; return new RenderNode.Styled(layout, /* ... */); } return new RenderNode.Noop(/* ... */); And of course doing control-flow analysis precisely isn't feasible due to the halting problem; you need to do some conservative approximation. So, if you want your programs to be portable from one version of the compiler to the next, somewhere you need to write down precisely what conservative approximation you're using for control-flow analysis, and in particular what you're not.
- ljloisnflwef 5y agoWe don't hold language authors accountable, and accept mediocrity.
- AtlasBarfed 5y ago... isn't this just OOP inheritance/subtyping/interface implementation with if/then checks for the more specific types?