Idris 2 0.6.0 is now available for the JVM
github.com
github.com
In terms of software engineering, one bonus you get is being able to encode your unittests in your type system. For example, you can do (in Agda):
_ : myFunction x y z == 33
_ = refl -- refl is of type `x == x`
and compiler will run this test for you at compile-time while type-checking your program.Two popular "practical" dependently typed languages are Agda and Idris. Of these two, Idris is relatively designed for software engineers, whereas Agda also wants to be a state-of-the-art automated theorem proving tool. I would strongly recommend checking both if you want to explore this area of programming languages. I wrote practical programs in Agda (e.g. language parsers, JSON parser etc) and I think it can make a programmer extremely productive, if you like this kind of workflow. Correctness is very easy to reason in these languages, and although it may look daunting at first, writing practical programs are very approachable (about as hard, maybe ever-so-slightly harder than writing code in Haskell). Have fun!
This is a problem in some cases e.g. if you write an infinite loop, this would mean compilation will infinitely loop. Different languages deal with this problem differently. E.g. in Agda, there is a "safe" subset of language that is not Turing Complete such that all programs can be algorithmically proven to be halting. But you can work around this via pragmas. Speaking from experience, the safe subset of Agda is all you need to write useful programs. You just need a tiny shell (maybe only few lines of code) that will handle IO and Haskell FFI. The rest of the code will be purely functional, safe, Turing Incomplete Agda.
The only possibility is to rewrite the code in similar way as clojure and Scala do it.
https://github.com/idris-lang/Idris2/blob/main/src/Compiler/...
One could argue that it's not "full" TCO, but it does cover a lot of use-cases. That rewrites to NamedCExp, so it could be reused by other backends.
The Java backend seems to be doing its own thing. There aren't a lot of comments or documentation in that repository, but I do see evidence that it's doing something interesting in src/Compiler/Jvm/Optimizer.idr:
Pure $ if shouldTrampoline && hasNonSelfTailCall tailCallCategory
then trampolineExpression True inlinedAndTailRecursionMarkedExpr
else inlinedAndTailRecursionMarkedExprhttps://github.com/mmhelloworld/idris-jvm/blob/latest/README...
Haskell, Rust,... and modern programming languages fail to do it. The only exception is Typescript.
For example, simple way to adding a extended boolean type:
type MyBoolean = null | true | false.
Or
type MyRange = 3 | 4 | 5
Curious.
3VL is a gigantic leap of of complexity over 2VL (boolean). The number of possible operations is a lot higher and the choice of operations is not standardized. To quote wikipedia: "In two-valued logic there are 2 nullary operators (constants), 4 unary operators, 16 binary operators, 256 ternary operators" And we all agree which of those 16 is named what. "In three-valued logic there are 3 nullary operators (constants), 27 unary operators, 19683 binary operators, 7625597484987 ternary operators" There is no standard meaning of the 3'rd value and of the operations involving it.
That is not to say that 3VL isn't useful. I believe the reason for the criticism of 3VL in SQL it that the system made those choices for you and those choices are not always intuitive. Therefore a null in one attribute in one relation might mean something very different from a null in another attribute in another relation.
Also note that as opposed to digital signal processing (where binary and ternary refer to the number of possible values a bit/trit can have), when discussing logic arity (nullary, unary, binary, ternary, n-ary) refers to the number of arguments required by a function as opposed to the number of values the arguments can have (univalent, bivalent, trivalent, k-valent). This is confusing because the terms are used interchangeably.
Assuming that `true | false` is equivalent to something like `enum { true, false }`.
Isn't it ? both cases represent a type than can express 3 variants
Classical example is wrapping multiple times: Option<Option<one | two>>. If you have null | null | one | two, well... that just boils down to null | one | two.
On the other hand, `Option<Option<one | two>>` allows you to distinguish between None and Some(None).
This makes union types unsound in the presence of type parameters/generics.
TypeScript supports both anyway, because Hejlsberg cares more about being able to type existing JS antipatterns than about providing a sound type system.
I'm not sure if "unsound" is a good adjective here. There are cases where this is actually desired behaviour and the rules can definitely be "sound".
For example, I might want to know what errors can appear, but not care where they come from. So `ErrorA | ErrorB` is what I want to see, not some nested structured that allows me to differentiate where ErrorA came from in case that there are multiple possible options.
So: > This makes union types unsound in the presence of type parameters/generics.
Sounds a bit strange to me. Why would union types + type parameters be generally unsafe? I doubt that that's true.
There is an isomorphism between them, but they aren’t equivalent since for one you will have to match on the `Option` first in order to see whether it is `None` or `Some(Next)` and then inspect `Next` (if `Some(…)`).
Same reason that `Nothing | Pointer` is not equivalent to `Option<Pointer>`. And it makes a huge practical difference, since the first type allows for “nothing-pointer dereference” while the second one does not.
That's not how strict TypeScript works. If you have a nullable you'll need to prove to the compiler first that it is not currently null before dereferencing.
> the type `null | true | false` is different from `true | false`, a type checker can assert that you handle the `null` case before using a function that wants a boolean. This is how rust handles it (with the Option<T> type).
If the variant `null` here is handled specially in general in TS then yeah, I was wrong. However, I was mostly replying to the part about “this is how Rust handles it”.
Sure, they can be better in java, but they cover a large enough ground, and as always, most people find that sufficient.
That can be done in Haskell with algebraic data types (with constructors without arguments), no?
Languages that have enums: Java, Kotlin, Dart, Ada, C (not in the first version), Pascal (not in the first version), Modula-2, Visual Basic,...
Pascal had subrange types since the first specification. For example "var month : 1..12;"
I don't know anything about Typescript but from what I have found it seems that Typescript's literal types are similar to Pascal's subrange types, i.e., the new type is compatible to the basic type it is derived from (unlike enums in the above languages, which are completely new types). But there is no runtime check.
Section "6.1.1 Scalar Types", 1973 edition:
https://www.standardpascal.org/The_Programming_Language_Pasc...
http://pascal.hansotten.com/uploads/books/Pascal_User_Manual...
I didn't realize that "scalar types" in the first edition were the same thing.
In general I think that it is not possible to "just" add an type intersection operator to an Hindley–Milner type system without making it either incomplete[1] or unsound[2]
In typescript this is often used with records as {a:number} & {b:string} = {a:number, b:string}
[0] https://v2.ocaml.org/releases/5.0/htmlman/types.html#sss:typ...
[1][2] which typescript is
EDIT: fix typo
But the ergonomics are quite different.
For example, in Scala you can do this:
val foo: "Must be this string" = "foo" // fails to compile
In Typescript it's similar. But how would "Must be this string" look in the case of a haskell ADT?Rust forces you to define a single enum to represent this, and then you can implement the TryFrom trait to make it directly comparable to booleans, integers etc, but without having to deal with random `undefined` values popping up everywhere.
Is it less convenient? Yes, but so are seatbelts. Sometimes you need to build software where this tradeoff is worth it.
Depends. TypeScript would (usually) recognize 3 being a part of that union type, but it's still a normal integer that is fully compatible with other numbers. If you add 1 to it the variable is still a number but doesn't belong to this union type anymore. You could of course argue that this is just a consequence of TS being wrapped around JS, but whether the type changes between `3` and `false` or between `3` and `4`, it does change.
In Rust this is not allowed, hence you cannot just do an addition between e.g. an enum variant and a number unless you explicitly implement the interface to make them compatible, in which case the addition produces a new value with - again - an immutable type.
If 3, "foo" and false have different types by themselves then the language has to either allow changing the type of a variable or forbid a mix of them in one type.
Of course it's possible to tell the TS type checker to sod off in a given context (cast, use the "any" type, etc.), but without doing that, I think it's hard to contrive a situation such as I described above.
https://www.typescriptlang.org/play?#code/C4TwDgpgBAsiAq5oF4...
Most other statically typed languages force you to fit your data to your types. Many statically typed languages simply degenerate into dynamic typing when trying to consume external data, e.g. by representing JSON as an enumeration of every possible JSON value type.
I'm a bit confused by the point about Haskell here -- IME Haskell's type system is pretty strictly nominal, not a lot of structural typing there
—- ADTs in regular old Haskell
data MyBoolean = MyNull | MyTrue | MyFalse
—- Refinement types in Liquid Haskell
{-@ type MyRange = {v:Int | ((v >= 3) && (v <= 5))} @-}But in Haskell (AFAIK, there may be a GHC extension) you cannot say to the type checker: I want to define a new type as a subset of the inhabitants of an existing type. That seems slightly more ergonomic in some situations if you then wanted to then run other functions that work on the original type — it at least saves you the conversion step.
Regarding MyBoolean, you’re now doing the previous trick, using a subset of the inhabitants of existing types, but they’re from different types and they’re being put in an untagged union, in Haskell this would be implemented with: type MyBoolean = Maybe Bool, where “Maybe” takes any type as an argument, IMO the Haskell way is a lot cleaner.
Did you mean to say "most useful" rather than "most frequent"?