I hope someone will design some jvm SimpleLang eventually, which will have very few syntactic constructions but cover all/most of use cases.
I hope someone will design some jvm SimpleLang eventually, which will have very few syntactic constructions but cover all/most of use cases.
As for there now being multiple ways to write the same thing, this seems like something that could be easily linted into consistency. So you would really only have to learn the less verbose way for any newer Java projects.
yes, new SimpleLang can have lessons learned during last 20 years implemented, as in example from another commenter: https://news.ycombinator.com/item?id=37068380
Currently, for a given task, Java usually have 5 legacy deprecated ways to solve, and two more in newish Java versions, and dev needs to keep all of them in memory to be able to read and understand other's code.
> As for there now being multiple ways to write the same thing, this seems like something that could be easily linted into consistency.
that's if you don't work with any legacy systems and 3p libraries and never switch projects/jobs
Kotlin sure moves faster than Java, but except for features that were experimental or unstable to begin with (coroutines, -X flags), I can't think of a case where more than 1 deprecated version exists (and the "deprecated" versions are not nearly as crufty as Java).
Sure there are a few warts, e.g. arrays work a bit differently than Lists, and primitive-object discrepancy, but every language will pick up similar issues over time.
But memory management/safety, weaker IDE support and ecosystem make it less productive than Java for business dev.
The distinction between fields and getters is intentionally blurred because these are immutable value classes.
Storing data is also where I thought records would be a good use case. But in the real world data is not Point(x,y). If you have more than 3 or 4 fields in your record, that's simply not going to work. I thought the rest of the industry decided over a decade ago that using positional properties (e.g. new MyRecord(a,b,c,d,e,f,g)) to initialize an object was a bad idea. I think Rich Hickey gave a whole talk on that subject. If there's a future plan to be able to create records using named props, e.g. new MyRecord([firstName: "Bob"]), then I would reconsider my opinion.
IMO it's a significant syntactical improvement, as it considerably reduces boilerplate assignments.
the problem is that Java not modern language, but carrying all legacy features from last 20 years, so having all legacy features + new modern creative ideas make Java very complex.
Overwhelmingly, I see complaints that java has been too simple compared to virtually all other languages. I agree that cognitive load is important. However, it feels really strange to me to suggest that java has reached the complexity of other similar languages. What am I missing?
- using DataInputStream
- using Scanner
- using Reader
- using Files.readLines
and they all suck.
I think they all connected, for example with added lambdas they also added streams, so collections library now allows to do the same thing using old iterators or new streams APIs, and somehow those APIs are not compatible, it is not easy to produce stream from iterator.
which I never seen anyone uses and it makes everything more complicated
they probably should've have some ParallelStream interface
core ecosystem, e.g. apache commons will add several more ways.
Java’s standard library is one of the best stdlibs out there.
programmers in 95% cases want something like following to read lines from text file:
for s in open('file.txt'): print(s)
Java has 5 different io APIs and no one provides such construct, you need to stack various verbose abstractions to just read text file.
How it is the best? Ergonomics and brain load is very bad.
string[] lines = ReadFile("path", "filename");
Why is the above somehow hard?- it will produce OOM if file is very large
- iterating array is usually verbose in many of languages (at least in Java)
- not sure why you would want to have path and filename as separate params
In Java, it would be more ergonomical if they add something like following:
Iterable<String> Files.readAllLines(String fileName);
Also, I really don’t understand your last example - that’s literally available in java returning a List of lines. If you add a .stream().collect(joining(“\n”)), you get the whole string in one go.
I think in python it will lazely iterate through the file, and not load everything at once
> Java’s List is much more ergonomic that arrays.
right, but commenter above proposed array..
> that’s literally available in java returning a List of lines.
current API has two issues:
- OOM if file is large
- it takes Path instance which you need to construct and not "String file" parameter, thus hit on ergonomics
- it will produce OOM if file is very large
A well designed API won't give you an OOM it'll throw a 'this file is too big' - not sure why you would want to have path and filename as separate params
Throws path not found. Throws file not found. Why does the programmer need to deal with twee-fiddly path magic for say each file in a directory of config files? Iterable<String> Files.readAllLines(String fileName);
Oh sure that's totally more readable. I think in python it will lazely iterate through the file,
I wrote a program to suck in a list of log files and generate a bunch of metrics and display them. Users liked to select a dozen 100MB log files. Lazy iteration was way way too slow. Fastest was suck each one into memory and call split.this is possible solution, still it doesn't allow to iterate lines one by one and handle large files
> Throws path not found. Throws file not found.
I think it is some very niche use cases when you need to distinguish between these cases. From another hand there are many places in the code which operate absolute file paths already, and you force them to split path, which is cumbersome. At least two alternative APIs probably are justified to have.
> Fastest was suck each one into memory and call split.
I really don't think pre-splitting is any kind of performance bottleneck, it is likely 1% of performance compared to all IO stack and subsequent string/objects memory management.
I mean, how are you going to use it? Something like this?
Point p = getPoint();
if (p instanceof Point(int x, int y)) {
System.out.println(x+y);
}
... that seems pretty bad. Or am I missing something?This type of pattern matching is mostly useful when you have some T and a few cases, there is also a switch/case equivalent for when you've got a lot of them.
Maybe we'll see some syntax for deconstructing values directly into variables, a la Python in the future.
You can then use pattern matching to switch on instances of those subclasses while destructuring objects to access attributes at the same time. Kind of like enums and pattern matching in Rust.
In your example there is no reason to use `instanceof` with `p`, you already know what it is.
Sure, but in Java the standard handling of that is polymorphism. instanceof has been so far a bit of an antipattern, breaking out of the OOP model.
> In your example there is no reason to use `instanceof` with `p`, you already know what it is.
That's my point. This seems like a very limited version of destructuring if you can use it only when you don't know the type. Like in JavaScript you'd routinely write something like:
const {x, y} = getPoint();A more practical (though not necessarily realistic) example might be:
sealed interface List<T> permits Cons<T>, None<T> {
record Cons<T>(T head, List<T> tail) {}
record None<T>() {}
}
// some function only wanting the first elem of a list
switch (List<T> list) {
case Cons<>(T head, var tail) -> // do something
default -> {}
}
If you have used Haskell or similarly strongly typed FP language, you can see how great pattern matching can be.I think that's deliberate, Java is moving towards the modern programming zeitgeist that doesn't really care about strict adherence to the OOP model. This is a feature that comes from languages that don't adhere to Java's OOP principles.
I didn't downvote you, by the way. Don't know why that happened.
This, along with the underscore, and even the sequence type, shows that the language designers are well aware of what Scala does, and are picking and choosing which things to bring in without risking exposing Java devs to the traditional monad salads that you see in many a Scala codebase.
But yeah, of course the design team knows about features from Scala, they are probably one of the most competent people on the Earth when it comes to PLs, and are literal CS giants.
(let [p obj]
(if (instance? Point p)
(let [x (.x p)
y (.y p)]
(println (+ x y))))As for the syntax, there is very little, which can make it a harder lift but once you have the hang of it you won't deal with the issues identified in the parent comment.
that's because of all legacy dynamicaly typed codebases
(if (? p::Point obj)
(... p:x p:y))
This succeeds if obj matches the pattern p::Point, declaring p to be the value of obj coerced to a Point.
This is integrated with Kawa's static type-checking/inference.but if it is a java record class, then there's currently no standard library way, since it's not a bean (getter/setter naming of properties). You'd have to write custom code for it, which sucks.
Using quasiquote-style pattern:
(if-match ^#S(point x ,x y ,y) obj
(prinl (+ x y))
Or using @(struct ...) pattern operator: (if-match @(struct point x @x y @y) obj
(prinl (+ x y))
All done with macros. Expansion, compilation, disassembly: 4> (flow '(if-match @(struct point x @x y @y) (new (point 1 2))
(prinl (+ x y)))
expand
prinl
compile-toplevel
disassemble)
(let ((#:g0062 (struct-from-args 'point 1 2)))
(let* (#:result-0065
x y)
(if (if (subtypep (typeof #:g0062)
'point)
(let ((#:x0063 (slot #:g0062 'x))
(#:y0064 (slot #:g0062 'y)))
(sys:setq x #:x0063)
(sys:setq y #:y0064)
(sys:setq #:result-0065
(prinl (+ x y)))
t))
#:result-0065
())))
data:
0: point
1: 1
2: 2
3: x
4: y
syms:
0: struct-from-args
1: subtypep
2: typeof
3: slot
4: prinl
5: sys:b+
code:
0: 2003000E gcall t14 0 d0 d1 d2
1: 04000000
2: 04020401
3: 20010005 gcall t5 2 t14
4: 000E0002
5: 20020002 gcall t2 1 t5 d0
6: 00050001
7: 00000400
8: 38000016 if t2 22
9: 00000002
10: 2002000A gcall t10 3 t14 d3
11: 000E0003
12: 00000403
13: 20020009 gcall t9 3 t14 d4
14: 000E0003
15: 00000404
16: 20020006 gcall t6 5 t10 t9
17: 000A0005
18: 00000009
19: 2001000D gcall t13 4 t6
20: 00060004
21: 1000000D end t13
22: 10000000 end nil
instruction count:
10
#<sys:vm-desc: 9966330>
I see this is doing something silly: (subtypep (typeof x) y) is just (typep x y), but costs an extra gcall instruction.Either the pattern matcher should do this, or the compiler should have that as an algebraic reduction, or both.
Let's do the compiler for fun:
$ git diff stdlib
diff --git a/stdlib/compiler.tl b/stdlib/compiler.tl
index 58685741..a837571e 100644
--- a/stdlib/compiler.tl
+++ b/stdlib/compiler.tl
@@ -1411,6 +1411,8 @@ (defmeth compiler comp-fun-form (me oreg env form)
(set form (rlcp ^(,bin ,a ,b) form)))
((- @a)
(set form (rlcp ^(neg ,a) form)))
+ ((subtypep (typeof @a) @b)
+ (set form (rlcp ^(typep ,a ,b) form)))
((@(or ignore nilf) . @args)
(if (eql sym 'ignore)
(each ((a args))
Note that we are using pattern matching here to recognize and rewrite this case. In return we will obtain better code from pattern matching: $ make
TXR stdlib/compiler.tl -> stdlib/compiler.tlo
$ ./txr
This is the TXR Lisp interactive listener of TXR 291.
Quit with :quit or Ctrl-D on an empty line. Ctrl-X ? for cheatsheet.
TXR is not a toy, but should be kept within easy reach of children.
1> (flow '(if-match @(struct point x @x y @y) (new (point 1 2))
(prinl (+ x y)))
expand
prinl
compile-toplevel
disassemble)
(let ((#:g0024 (struct-from-args 'point 1 2)))
(let* (#:result-0028
x y)
(if (if (subtypep (typeof #:g0024) ;; this hasn't changed, of course
'point)
(let ((#:x0026 (slot #:g0024 'x))
(#:y0027 (slot #:g0024 'y)))
(sys:setq x #:x0026)
(sys:setq y #:y0027)
(sys:setq #:result-0028
(prinl (+ x y)))
t))
#:result-0028
())))
** expr-1:1: warning: if-match: no such struct type: point
** expr-1:1: warning: new: point does not name a struct type
data:
0: point
1: 1
2: 2
3: x
4: y
syms:
0: struct-from-args
1: typep ;;; no mention of subtypep here any more!
2: slot
3: prinl
4: sys:b+
code:
0: 2003000E gcall t14 0 d0 d1 d2
1: 04000000
2: 04020401
3: 20020002 gcall t2 1 t14 d0
4: 000E0001
5: 00000400
6: 38000014 if t2 20
7: 00000002
8: 2002000A gcall t10 2 t14 d3
9: 000E0002
10: 00000403
11: 20020009 gcall t9 2 t14 d4
12: 000E0002
13: 00000404
14: 20020006 gcall t6 4 t10 t9
15: 000A0004
16: 00000009
17: 2001000D gcall t13 3 t6
18: 00060003
19: 1000000D end t13
20: 10000000 end nil
instruction count:
9 ;; down by one
#<sys:vm-desc: 92c94d0>
Ship it!Just look at C# or C++, both have at least an order of magnitude larger language “surface area”.
Also without the :: operator, it’s uglier than the arrow equivalent and it’s yet-another-construct to parse.
> There really aren't that many to learn.
I think there are a lot to learn in java ecosystem.
There are many of us who think Java has very little to learn - asking us to go slower - while probably good for society in general, is a losing strategy in a Capitalist economy.
You can keep up, or be left behind. Meanwhile, fortunately the “communicate effectively” is 100x more valuable as “produce software more reliably and faster” ie learning all of these new languages and language improvements.
I think the closest thing is if someone would create linter for java which would allow only essential structures and library API. Java has very good foundational core.