Hacker News new | ask | show | jobs
by troad 29 days ago
String literal typing appears to be a common feature of type systems bolted onto dynamic languages:

    # Python
    MyStringBool = Literal("Yes") | Literal("No")

    // TypeScript
    type MyStringBool = "Yes" | "No"
I assume it exists to compensate for the previous lack of typing, and consequent likelihood of ersatz typing via strings.

It would seem pretty unnecessary in Haskell, where you can just define whatever types you want without involving strings at all:

    data MyBool = Yes | No
Of course you'd need a trivial parser, though this is probably a good idea for any string type:

    parseMyBool :: String -> MyBool 
    parseMyBool "Yes" = Yes
    parseMyBool "No" = No
    parseMyBool _ = error "..."
Interestingly, dynamic languages which make use of symbols (Ruby, Elixir, Common Lisp) probably fall closer to Haskell than Python or TS. Elixir example:

    @type my_bool() :: :yes | :no

    @spec parse_my_bool(String.t()) :: my_bool()
    def parse_my_bool("Yes"), do: :yes
    def parse_my_bool("No"), do: :no
    def parse_my_bool(_), do: throw("...")
Where :yes and :no are memory-efficient symbols, not strings.
2 comments

String literals are structural types which are way more expressive than regular (Haskell) ADTs, which are nominal types.

In TS in particular, in combination with other features (mapped types), they are equivalent to row polymorphism + whatever Haskell/GHC features enable type families to specialize on constant literal arguments (or you can use atomic types, but that's not structural / open-world)... so pretty advanced.

This is valid TS/Python:

    type ABC = "A" |"B" | "C"
    type AB = "A" | "B"
    const x: AB = "A";
    const y: ABC = x;
The equivalent Haskell requires using several extensions.
I know. I literally gave the example of a Python Literal in the post you're replying to. TS too. :)

My overall point is that Haskell's type system is sufficiently expressive (you may not have "A" | "B" | "C", but you do have A | B | C) that there's no obvious remaining use case for string literals, unless you're thinking of typing input by way of expected literals instead of actually parsing it, which is... a choice. :P

By Haskell's type system do you mean with all the GHC extensions?

Because TypeScript has structural sub-typing, while standard Haskell (eg. `A | B | C`) has neither subtyping nor structural typing, which both are very useful features for safe "integration/glue" type of programs.

(String) literals form a fundamental part of the TS "row polymorphism" (record types) and eg. tuple union type implementation.

You can type a non-empty array that starts with zero...

    type Arr = [0, ...number[]];
    const a: Arr = [0, 1, 2, 3, 4]
Now try in Haskell.
> By Haskell's type system do you mean with all the GHC extensions?

No? What extensions does `A | B | C` require?

> Haskell has neither subtyping nor structural typing

Is subtyping back in? Good news for Java and C++.

Re structural typing, I would ask what behaviour you're after, specifically. For example, this is a valid, typed Haskell function for any two values that can be added, including any user-defined ones:

    adder a b = a + b
If by structural typing you mean silently coercing types that the compiler deems structurally equivalent, then no, but I don't think many people writing Haskell would consider that desirable. A `Person` may have an age (40) and a `Wine` may have an age (2005), but you're not going to get sensible results if you start adding those two together, and your compiler should probably stop you.

Structural typing is the sort of thing that is very valuable if you're bolting a type system onto a language with a cornucopia of untyped structs, like JS objects. It is comparatively much less valuable if you're working in a typed ecosystem to begin with, since you're not liable to have loose untyped structs floating around that require coercion.

> "integration/glue" type of programs

It does sound a lot like you're using string literals in lieu of parsing foreign input, which strikes me as a pretty bad idea. Particularly in a language like TS, which is not type safe at runtime, and which will happily ingest an unexpected value, silently coerce it in all sorts of fun and wacky ways, and cause behaviour far removed from what any static analysis of the TS source would suggest.

> You can type a non-empty array that starts with zero

Can you please name me any possible actual use for this? Especially given the type doesn't even exist at runtime and will never be enforced on input data, so this is a once-off check for comptime constants?

OCaml and Scala, both also famously strongly typed functional languages, also have structural typing (OCaml even has many different kinds at many different levels!). Mainstream, Go is based on structural (interface) typing.

The Person/Wine example is a pointless strawman. That's not what structural typing is generally used for.

The entire comment is basically making up strawmans... I didn't give practical examples to save space, obviously, it was just to disambiguate what I meant.

TypeScript has several runtime-safe advanced validators based on its type system (most well-known being Zod), capable of enforcing types similar to what I provided.

To conclude, these type system features were added by multiple experienced language designers for a reason, to languages that already had functional ADTs, so going "huh but what are these even useful for?!" to me sounds a bit clueless (or argumentative), so I don't see a productive continuation to this discussion.

Haha, I knew you'd bring up Go. I even considered pre-emptively dropping Rust and Zig as counterexamples. I think most people who favour static typing consider the duck-typed interfaces of Go to be a mistake. Personally, I consider all of Go to be a mistake.

> these type system features were added by multiple experienced language designers for a reason

Oooh, an appeal to authority, where that authority isn't even named. I'll have you know a famous queen told me you're wrong on this, also for "a reason".

> TypeScript has several runtime-safe advanced validators based on its type system (most well-known being Zod), capable of enforcing types similar to what I provided.

Right, so TS typing is so amazing it requires runtime parser libraries from NPM, and Haskell is less sophisticated because it's not stringly typed.

You realise the entire, complete, exhaustive runtime schema for your zero-first non-empty integer array example looks like this in Haskell, right?

    sch (0:_) = True
    sch _ = False
That's a complete function that somehow manages to work without pulling in NPM dependencies. The best JavaScript minds of our generation remain baffled.

> The Person/Wine example is a pointless strawman

> I didn't give practical examples to save space, obviously, it was just to disambiguate what I meant.

So when you use illustrative examples, it is to "disambiguate what you meant" (huh?), and when I do it, they're "pointless strawmen". A little hypocritical, no?

> a bit ignorant

Honestly, I don't think you know what you're talking about at all. You clearly hadn't even read my comment when you started replying with Python and TS examples... that were already in my comment.

It also really sounds like you're using string literals to type input without properly parsing it, which is just a terrible idea. Haskell's type system is designed precisely to protect you from this sort of mistake. [0] No, you're not always going to get what you expect. No, your JS program will never let you know that's the case. No, a sane type system does not require mainlining runtime parser libraries from the biohazardous oceans of NPM. A schema in Haskell is going to be significantly shorter and sounder than anything in Zod, and you don't need a library for it.

As I said above, TS' type system makes sense for a type system bolted onto a dynamic language post facto. TS needs to more tightly link (even mildly conflate) values and types, since it needs to do a lot of clever narrowing to figure out what mad ball of JS it is dealing with at any given time. Haskell does not operate under any such constraint.

> I don't see a productive continuation to this discussion.

Phew. Timesaver.

Of course the irony of all this is that I use TS daily, and Haskell quite rarely.

[0] https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...

Thank you for the lecture but I didn’t mean that. I meant `Either String String` is possible in Haskell and not in C# because… C# is strongly-typed.
You're welcome! Knowing is half the battle.

That Haskell snippet is just syntax sugar for Left(string) | Right(string), which is trivial in any language with unions.

Not clear why it would be an improvement over just naming the alternatives something meaningful, but if you're wedded to Left and Right, go for it.