Live data from Hacker News

Why don't more languages offer flow typing?

ayazhafiz.com

71–80 of 127 posts

Re: Why don't more languages offer flow typing?

#71

Nobody'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 Con…

This 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 ?

Re: Why don't more languages offer flow typing?

#72
post #69

Earlier quoted context omitted.

> Go's interfaces are a decent 80/20 answer. You pay for them, you opt in, and they sit on type of the base type system, they aren't the primitive the entire type system is based on That's interesting because I see them as the exact opposite. You're not saving or gaining anything of significance, and it's just increasing confusion. Penny wise, pound foolish, if you will.

I'm not sure what you mean by "not gaining anything of significance." Go without interfaces (and for simplicity let's ignore 1.18's generics for a moment) would not be a useful language at any significant scale. You'd end up with a lot of "by hand" interfaces of structs full of function pointers (or method closures), only without the compiler support. Nor am I sure exactly what "confusion" is being increased.

> I'm not sure what you mean by "not gaining anything of significance." Go without interfaces (and for simplicity let's ignore 1.18's generics for a moment) would not be a useful language at any significant scale.

Why would go not have interfaces? Where did you make that up from?

Go would have nominative interfaces Like most other languages.

Re: Why don't more languages offer flow typing?

#73
post #4

This 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 m…

Dynamic and strong typing are not opposed (dynamic and static typing are opposed, and, to the extent the distinction is valid, weak and strong typing are opposed), dynamic doesn't mean everything is physically boxed (most dynamic language implementations don't box some subset of small primitive values, usually including bools and small ints), and, in any case, flow typing is a feature of static type systems (though s…

Remarkably, a post written in generalities speaks in generalities. None of those things may be "necessary", but there sure is a lot of all the things I said, aren't there?

What you call "speaking around" I call an important thing to understand about a lot of languages: At some point, your data will be physically laid out in memory. If you don't care about performance... and I mean that as a perfectly valid choice in many situations, not a snark... it doesn't much matter how it is. But if you do, and you selected a language based on that, it matters a lot, and you have to view every type system detail through the lens of "what does it look like in memory?" if you want to understand it. The choices this class of language makes for their type systems will never fully make sense if you are not paying attention to this, and also if you gloss over the legitimate difficulties that arise for any sort of "why don't they just...?" sorts of questions.

(In particular, you really don't understand how good Rust is until you look at it through this lens, then look at just how much simultaneous power and convenience they've drawn out while never compromising on the memory issues. It's an incredible piece of work. It probably isn't "optimal" because such things don't exist in our real universe, but it probably is as close as we'll ever see for that class of langauge.)

Re: Why don't more languages offer flow typing?

#74
post #4

This 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 m…

It sounds like you are very confused about types and values. Static type checking does not incur any extra allocations at runtime.

Re: Why don't more languages offer flow typing?

#75

Earlier quoted context omitted.

I don't think pattern matching and union-types makes narrow typing useless. Rust has both pattern matching and union types but still implements some specific forms of control-flow based typing. For example, consider this Rust snippet : pub fn positive1(x: isize) -> Option { if x > 0 { Some(x as usize) } else { None } } Unless I'm mistaken, without narrow typing, this cast would be impossible, and there would be no wa…

> Unless I'm mistaken, without narrow typing, this cast would be impossible same sized integer casts in Rusts are no-ops [0]; the conditional isn't type narrowing, it just avoids the cases where the cast would not preserve the same semantic value. [0] https://doc.rust-lang.org/reference/expressions/operator-exp...

Right. Thank your for the correction.

Re: Why don't more languages offer flow typing?

#76
post #15

This 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.

In 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 th…

>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-...

https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/is-...

Re: Why don't more languages offer flow typing?

#77

Earlier quoted context omitted.

Yeah, 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) }

You mean sum types, not union types (TypeScript has union types, your example uses sum types.)

An 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.

Re: Why don't more languages offer flow typing?

#78

Contrary 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…

You want something like Haskell's Typeable class?

Re: Why don't more languages offer flow typing?

#79
post #15

This 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.

In 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 th…

Yeah, 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(val: T): asserts val is NonNullable {
      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();
    }

Re: Why don't more languages offer flow typing?

#80

Contrary 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…

You 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.

Post reply on HN