15 ms·
> That's a strange claim. I mean, yeah, maybe you can say that it's more advanced than "vanilla" Haskell, but how many people use no extensions at all? Let me
by dwohnitmok 6y ago
> That's a strange claim. I mean, yeah, maybe you can say that it's more advanced than "vanilla" Haskell, but how many people use no extensions at all?
Let me broaden that then. Typescript's type system is more expressive than even the proposed list of GHC2021 extensions. You really need to turn on "GHC and the kitchen sink" (basically your code is probably going to be using the singletons library and everything that entails) to get to the feature set of Typescript's type system. I think that's beyond what the vast majority of Haskellers use or are comfortable with.
> But when compared to Scala 3 - is there anything major that you can do with typescript but not with Scala 3?
Plenty. Part of it has to do with the fact that interfaces are truly structural types in Typescript whereas traits are still nominal in Scala 3. So for example, Typescript lets you have "complement types" (i.e. give me all the fields of A except those in B) which you cannot do in Scala 3.
Here's an example of a useful type-level thing I've found myself wanting all the time in Haskell and Scala that is very straightforward in Typescript.
Often we have a record
data MyRecord = MyRecord
{ field0 :: Int
, field1 :: Bool
, field2 :: Bool
}
that must undergo validation first before we can generate the record (e.g. parse out a bunch of strings for each field into Ints and Bools where those parse operations may fail).
It is tedious to write a whole new data type which duplicates all the fields, but just adds a Maybe in front of each type. Haskell has a design pattern known as "higher-kinded data types" that tries to mitigate that.
data MyRecord f =
{ field0 :: f Int
, field1 :: f Bool
, field2 :: f Bool
}
In its more sophisticated form we can use type families instead of normal higher-kinded types along with TypeApplications to make this fully seamless and get rid of a lot of Identity wrappers. Scala 3 allows something similar with match types (in fact you don't even need match types for getting rid of Identity wrappers due to Id in Scala being a type synonym and being able to do the equivalent of TypeApplications for higher-kinded type synonyms).
This approach, however, doesn't quite work if you have multiple validation steps that return multi-field fragments of MyRecord (e.g. one step returns a fully validated field0 and field1 and another returns a fully validated field2). You basically need to resort to writing a bunch of duplicated types again (type families can kind of save you if you happen to have all entirely distinctly types).
Typescript's mapped and conditional types handle this with aplomb, letting me generate any combination of fields wrapped in any combination of types from a single type definition.
Separately, in another comment in this same chain I give an example of (limited) dependent types in Typescript. I think you can replicate it with singletons in Haskell, but that's definitely getting into stuff that I would say "doesn't compose and does not age very well" in Haskell (ah should the blessed day that dependent Haskell lands arrive soon!). AFAIK you can't replicate that in Scala 3 because of a lack of value to type promotion mechanisms (I think even with match types it's not quite sufficient).
- valenterry 6y agoActually, both your examples are possible to solve even in Scala 2. Check my repository from 4 years ago. The Readme has the examples that will be interesting for you: https://github.com/valenterry/bamboomigrate https://github.com/valenterry/bamboomigrate I'm not saying it looks very elegant though - this is certainly improved a lot in Scala 3 Actually, the match types page in Scala 3 lists some of the differences with typescripts approach: http://dotty.epfl.ch/docs/reference/new-types/match-types.html#related-work http://dotty.epfl.ch/docs/reference/new-types/match-types.ht...
- dwohnitmok 6y agoAh I think we're talking about subtly different things. You can definitely use generic goodness (`Generic` in Haskell and shapeless in Scala) to create generic conversions between different types, but the result of each conversion is not itself a user-friendly type. In the case of shapeless the general pattern is to have a starting type, use something like LabelledGeneric, do a ton of HList transformations, and then transform back. But while you're living in HList land those are in fact HLists and not, say, case classes. Indeed, you must always write out the final type yourself (e.g. create a new case class). Even if you're using only HLists and completely eschewing case classes (which is both ergonomically painful but also has a pretty large performance hit), shapeless's functions that generate new HLists (as opposed to extracting an element) almost always require you to annotate their use sites IIRC, which is effectively the same thing as needing to write out the whole case class. To put it another way, with shapeless in Scala 2 you could prove that a certain type was the intersection of two other types and create the generic transformation to do so. But this isn't true intersection types because you still need to write your starting types and your final type. That is in Scala 3 you `T0 & T1` is a type in and of itself; you don't need to rewrite all the fields in common between T0 and T1 and assign it to a new type (and then e.g. use shapeless to prove the relationship between your types). In the same way what you've demonstrated here aren't mapped types, conditional types, etc., but rather a way of using shapeless to prove that a certain type is the mapped type/conditional type etc. of another type. The former is what I meant when I said "letting me generate any combination of fields wrapped in any combination of types from a single type definition." Shapeless doesn't get rid of the problem that "Higher-Kinded Datatypes" (HKD) is trying to solve, since you still have to write all the duplicate types by hand for any reusable function. Moreover, apart from the tedium of rewriting all these types, the bigger issue is that TS's type system lets you stay entirely at the value level, whereas shapeless requires things to become type level computations, effectively bifurcating your code into "shapeless transformations" and "ordinary functions acting on e.g. case classes." For example, the equivalent of your library in TS would have each step just be a normal function like any other rather than a special type level computation. It can be applied to a normal object directly and can be tested and reused just like any other function. That match types page on Scala 3 I think undersells the differences. The real power of a lot of TS's type level computations is how they interact with each other and typeof (see my other example in this thread of lightweight dependent types). All that being said, I really like the concept behind your library and would love to see it updated for Scala 3!