"So I find words I never thought to speak
In streets I never thought I should revisit
When I left my body on a distant shore."-- T. S. Eliot
Java has been a key player in the programming landscape since it was created in the mid-90s. One of the big attractions of Java is its "write once, run anywhere" execution model, where Java source code is compiled to bytecode by the compiler. This bytecode can later be executed by the Java Virtual Machine (JVM), which converts the Java bytecode to machine-specific instructions.
Separating the writing of code from the execution of code into two domains means that Java developers don't have to write architecture-specific instructions. They can just write Java code, and expect that the people building the JVM will take care of all of the machine-specific nuances.
Of course, the JVM doesn't know how the bytecode was generated, just that it's supposed to run it. Language designers soon realised that this meant they could create new languages which ran on the JVM, provided they created intermediate compilers that could convert those languages into Java bytecode. And so, almost immediately after the first version of Java was released, new JVM languages started to appear. What follows are some of the coolest features I've found in these non-Java JVM languages.
A small clarification: some of the features below are found in multiple languages on this list. Languages which developed later are naturally influenced by those that came before them, and languages which have more resources behind them can incorporate more features. This is why -- for instance -- the feature set of Kotlin (funded by JetBrains, which charge €500/year for their IDE, IntelliJ) is essentially a superset of all of the features of other popular modern languages like Scala and Groovy.
I made no attempt to uncover which language first implemented which particular feature below, rather, I wanted to simply highlight some interesting features which haven't (yet) been incorporated into Java, to the best of my knowledge. If you find any mistakes below, or know of any other neat features available in non-Java JVM languages, please let me know in the comments!
Table of Contents
- Ioke -- homoiconicity
- Gosu -- open type system
- Gosu -- optional and named parameters
- Prompto -- standard dialects
- Frege and Eta -- "Haskell for the JVM"
- Ateji PX -- parallel blocks
- Fantom -- "once" methods and lots of literals
- Whiley -- verification via formal specification
- Whiley -- flow-sensitive typing
- Jython & JRuby -- language interoperability
- Clojure -- "LISP for the JVM"
- Scala -- "everything is an object"
- Scala -- everything is an expression
- Scala -- case classes and pattern matching
- Scala -- implicit programming
- Groovy -- partial application and composition of functions
- Groovy -- easy regular expressions
- Groovy -- JSON-to-classes
- Kotlin -- JavaScript transpilation
- Kotlin -- coroutines
#1. Ioke -- homoiconicity
Homoiconicity is a subject which takes a bit of explanation, but what it boils down to is that a homoiconic program is composed of language elements which can be interpreted as objects within the language itself.
You could imagine a programming language where every object is a directed graph -- loops are created with actual loops in the graph, and all information needed to run the program is contained within the vertices of the graph. If this language itself can manipulate graphs, then it can operate on its own source code. "Code as data" is a common maxim when working with homoiconic languages.
In Ioke, "everything is an expression composed of a chain of messages". The entire language syntax can be summarised as follows:
program ::= messageChain?
messageChain ::= expression+
expression ::= message | brackets | literal | terminator
literal ::= Text | Regexp | Number | Decimal | Unit
message ::= Identifier ( "(" commated? ")" )?
commated ::= messageChain ( "," messageChain )*
brackets ::= ( "[" commated? "]" ) | ( "{" commated? "}" )
terminator ::= "." | "\n"
comment ::= ";" .* "\n"
Since every program is a message chain, and a message chain is itself an object in Ioke, Ioke programs can manipulate their own source code (or the source code of other Ioke programs) as data. While the jury is still out as to whether homoiconicity is a good thing™ for a programming language to achieve, it certainly is an interesting feature, if nothing else.
#2. Gosu -- open type system
Gosu is A JVM language that provides an open type system, meaning Gosu programmers can add functionality to classes, even if they didn't create those classes themselves. This includes built-in Java classes like java.lang.String
. A simple example is given on Gosu's Wikipedia page:
enhancement MyStringEnhancement : String {
function print() {
print(this)
}
}
The above enhancement
adds a print()
method to the java.lang.String
class, meaning a String
can now print itself:
"Echo".print()
Gosu also provides extremely simple closures and enhancements
to the Iterable
interface, which -- combined with the language's scripting-like syntax -- make Collection
manipulation a breeze:
var lstOfStrings = { "This", "is", "a", "list" }
var longStrings =
lstOfStrings.where( \ s -> s.length > 2 )
.map( \ s -> s.toUpperCase() )
.order()
print(longStrings.join(", ")) // prints "LIST, THIS"
#3. Gosu -- optional and named parameters
Gosu also brings to the JVM a feature which exists in many other languages, but is (as of this writing) still missing from Java: optional parameters.
In Java, there's no way to specify a default value for a parameter in a constructor. If you want to make a parameter optional, you have to use a Builder Pattern, remove the parameter from the constructor entirely and rely on the user to set it after the fact, or create multiple constructors:
public class Person {
private String opt = "default";
public Person (String opt) {
this.opt = opt;
}
public Person() {
// ...
}
//...
}
Gosu is one of many modern languages that makes this functionality a bit easier by offering optional parameters. Here's some Gosu code similar to the Java code above:
class Person {
var _opt : String
construct (opt : String = "default") {
_opt = opt
}
//...
}
This constructor provides a default value "default"
to the parameter opt
. This constructor can be called with a single String
parameter, or with no parameters at all. In that case, the default value will be used.
Optional parameters can cause problems when constructors have more than one argument, though. Consider this case, for instance:
class Person {
var _opt : String
var _ional : String
construct (opt : String = "default", ional : String = "ditto") {
_opt = opt
_ional = ional
}
//...
}
This new Person
implementation has two optional parameters. So if we call its constructor with:
new Person("potato")
...which parameter should be assigned the value "potato"
? They're both String
s and they both have default values, after all. What if we want to use the default value for opt
, but a non-default value for ional
? Gosu resolves this issue with named parameters. When we call the construct
method, we can specify which parameter we want to assign this value to:
new Person(:ional = "potato")
The named parameter makes it clear that we want to assign this value to ional
, and not opt
.
#4. Prompto -- standard dialects
Prompto is a cloud-based programming language designed for building complete information systems, like client-facing web apps which need to interact with server-side data (e-commerce sites, for instance). Prompto is the name of the programming language used to build these applications, but also the platform on which they run.
Prompto (the platform) provides various development tools, including a web-based REPL, database connectors, a debugger, a JVM-based compiler, and a JavaScript transpiler.
Prompto (the language) was started as an experiment in reifying attributes, and offers some radical features for a JVM language, including safe multiple inheritance, easy integration with various languages (including Java, JavaScript, C#, and Python, with Swift in the works), and -- my favourite feature -- dialects.
With Prompto dialects, "syntax is a detail". Programmers can write in whichever style suits them best, and code from one dialect can be losslessly translated into other dialects.
Prompto's three built-in dialects are:
-
Objy, for OOP-flavoured syntax
method main() { print("15 + 3.5"); print("= 28.5"); }
-
Monty, for Python-like syntax
def main(options:Text<:>): print("15 + 3.5") print("= 28.5")
-
and Engly, for English-like syntax
define main as method doing: print "15 + 3.5" print "= 28.5"
All three blocks of code above are the same Prompto code, expressed in different dialects. Every programming language must provide some syntax to perform a handful of basic tasks: defining variables, running code in a loop until some condition is met, printing information to the user, etc. With Prompto, you're not stuck with a single way to express these concepts, you can choose the style of programming which suits you best!
I'm personally very excited about the idea of dialects in programming languages and I wish more languages would follow Prompto's lead and incorporate this cool feature.
#5. Frege and Eta -- "Haskell for the JVM"
Frege and Eta are two JVM-based languages which both purport to be "Haskell for the JVM". (Haskell is a programming language which has become nearly synonymous with the functional programming paradigm since the language's first release almost 30 years ago.) So one might expect both of these languages to bring pure functional programming to the JVM... so why are they two separate languages?
Eta isn't just meant to look like Haskell, it is Haskell (or a dialect of it), just running on the JVM. Eta strives for maximum compatibility with the Glasgow Haskell Compiler (GHC), and the common repository of Haskell libraries, Hackage. In addition, Eta offers Java interoperability with its strongly-typed Foreign Function Interface (FFI), meaning you can write functional code, but still use the Java classes and features you're used to.
Frege, on the other hand, "while it supports basic Haskell, lacks many of the key extensions required to compile Hackage, and hence cannot reuse the existing infrastructure." Frege's "Differences Between Frege and Haskell" page on their GitHub was last updated in May 2017, and explains how Frege errs on the side of Java rather than Haskell:
"The Frege-Prelude defines many functions, type classes and types known from Haskell in a similar way. The goal is to implement most of the Haskell 2010 standard library in a most compatible way as far as this makes sense."
"Apart from that, everything else will be quite different, because Frege uses the Java APIs whenever possible. At the time being, there is not much library support yet beyond the standard libraries."
Frege is more established. Eta development has been more active recently. Both projects have one or two key developers, and so are maintained by very small teams. Although they both have roughly the same number of GitHub stars, "eta lang" returns almost 75x as many results on Google as "frege lang". Eta also has an official website.
While both languages are intriguing, and both advertise themselves as "Haskell for the JVM", if I had to learn only one of these two languages, I would probably pick Eta. Es tut mir leid, Herr Doktor Frege.
#6. Ateji PX -- parallel blocks
"Do one thing, and do it well", the mantra of UNIX architects in the 1970s, was well-heeded by the developers of Ateji PX, a programming language which extends Java with a single key feature -- easy parallelism.
Ateji is pronounced "ah-teh-gee", and the "PX" stands for "parallel extensions", which are what it provides for the Java ecosystem.
Writing parallelised (or "concurrent") code is still not easy, more than a decade into the era of multi-core processors. Different languages attack this problem in different ways. Some allow users to manually create threads, some allow higher-level management of pools of threads, some provide serialised and parallelised versions of the same methods, some implement actor models, and many languages do many (or all) of these. The programming language community doesn't seem to have yet come to a consensus on when and how parallelism should be applied, in spite of a well-established pi-calculus having existed in theoretical computer science for decades.
Ateji PX threw its metaphorical hat into the ring around 2010, with its supremely simple parallelised blocks:
public class HelloWorld {
public static void main(String[] args) {
[ System.out.println("Hello"); || System.out.println("World"); ]
}
}
Above, the []
delimit a parallel block, inside of which, the operator ||
is used to create a parallel branch. The above code could print either
Hello
World
or
World
Hello
...depending on the order in which the parallel threads finish. On its own, this is pretty impressive, but Ateji PX uses the ||
operator in multiple places throughout the API, allowing users to easily parallelise loops:
for||(int i : N) array[i]++;
...implement recursive, parallelised algorithms:
int fib (int n) {
if (n <= 1) return 1;
int fib1, fib2;
// recursively create parallel branches
[
|| fib1 = fib(n-1);
|| fib2 = fib(n-2);
]
return fib1 + fib2;
}
...write elegant one-liners using sophisticated comprehension expressions:
// sum all elements of the upper-left corner of matrix in parallel
int sum = `+ for|| (int i:N, int j:N, if i+j<N) matrix[i][j];
...use a concurrent channel (locally or across multiple machines) for passing messages between threads:
// declare a channel visible by both branches, and instantiate it
Chan chan = new Chan();
[
// in the first parallel branch, send a value over the channel
|| chan ! "Hello";
// in the second parallel branch, receive a value from the channel and print it
|| chan ? s; System.out.println(s);
]
...and use speculative parallelism to run multiple algorithms at once, returning a results as soon as the fastest one has finished:
int[] sort(int[] array) {
[
|| return mergeSort(array);
|| return quickSort(array);
]
}
You can read all about these (and more) features in the whitepaper here.
Unfortunately, Ateji, the company which created and maintained Ateji PX (as well as OptimJ, another extension to Java), folded in 2012, so development of the language has stalled.
If you're interested in reviving Ateji PX or applying its principles to your (or another) programming language, please send me a private message and I'd be happy to get you in touch with one of the original developers of the language, who has said he would be more than happy to help by contributing his expertise. You can check out the language manual here to get started.
I have to say that I'm actually kind of mad that I've never seen this notation before. It's -- if you can say this about a programming language -- beautiful. Abstracting away all the nonsense about Thread
s and Runnable
s and thread pools and replacing all of it with a single operator, ||
. What could be simpler?
#7. Fantom -- "once" methods and lots of literals
A neat feature of purely functional languages (because of the "no side effects" rule) is that they're often able to simplify values and methods by taking logical shortcuts. In some languages, for instance, functions which take no parameters but return a value may simply cache that value after having calculated it once. Then, when the function is called again, the value is simply returned and the body of the function is effectively ignored.
This technique, called memoization can be used very effectively when, for instance, the user needs to interact with the disk. In a file explorer, for example, instead of interacting with the disk whenever the user moves into or out of a directory, we can simply cache the entire file tree (or a subset of it) ahead of time and the user can interact with that cached file tree, effectively eliminating disk read latency.
The Fantom programming language brings some interesting new features to the JVM, including memoization via its once
methods:
once Str fullName() { return "$firstName $lastName" }
(Fantom replaces Java's
String
type withStr
.)
once
methods must be instance methods (not static
), they must take no parameters, and they must return a value. They can throw Exception
s, but then the body of the method will be re-run on subsequent calls until some value is returned. Once a value is returned from the method, that value is cached and the body of the method is never again executed.
As you can see above, Fantom also allows for string interpolation. The values firstName
and lastName
are inserted in-place where they're included in a string with a preceding $
character. In addition to string interpolation, Fantom offers lots of small additions to Java's repertoire of literals, allowing object creation without explicitly invoking a new
object. For instance:
0.2e+6D, 123_456d // BigDecimal literals
100ms, -0.5sec, 2hr // Duration literals
`/some/path/file.txt` // URI literals
0..5, 3..x, a..<b // range literals (< == exclusive)
[1: "one", 2: "two"] // Map literals
[10, 20, 30] // List literals
...
Many, many more literals and improvements to string handling are available with Fantom. Fantom is a multi-paradigm language that supports functional programming through closures, concurrency through the actor model, and takes a "middle of the road" approach to static vs. dynamic typing. Fantom is truly a jack of all trades.
#8. Whiley -- verification via formal specification
Whiley, a JVM language introduced to the world around 2010, is David Pearce's attempt at introducing a programming language with a verifying compiler, as described by Sir Tony Hoare:
"A verifying compiler uses mathematical and logical reasoning to check the correctness of the programs that it compiles."
Hoare is famous for (among other things) his invention and subsequent condemnation of the null reference:
"I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years." [ source ]
Perhaps as a means of atoning for his "billion-dollar mistake", Hoare later developed a set of rules for reasoning about the correctness of programs, now known as Hoare logic, or Floyd-Hoare logic. In 2003, Hoare put forth a challenge to computer scientists to develop a verifying compiler, warning that this might "involve [the] design of a new programming language and compiler, especially designed to support verification". Whiley attempts to do just that.
Unlike other programming languages, which had features tacked on in an attempt to introduce verifiability (like ESC/Java, Frama-C and others), Whiley was built from the ground up to facilitate verification.
Whiley is built on a pure functional foundation, and takes care to distinguish between functions (no side effects, no state) and methods (may introduce side effects, may have some internal state). Functions can be reasoned about with regards to verification, while methods are more complicated. Whiley also uses a complex type system and defaults to immutable values for compound variables like arrays and maps.
The following example Whiley function:
function abs(int x) -> (int r)
ensures r >= 0
ensures r == x || r == -x:
//
if x >= 0:
return x
else:
return -x
...has a postcondition requirement that the returned value must always be non-negative (r >= 0
). During compilation, Whiley verifies that the returned value, r
, meets this postcondition, through a logical analysis of the function definition (found below the postconditions).
By adding postconditions like this to functions throughout a program, Whiley can verify, at compile time, that a program is constructed correctly. This verification process eliminates errors like divide-by-zero, array out-of-bounds and null dereference.
Whiley is still under active development, with the most recent version (0.4.2) available as of April 2018.
#9. Whiley -- flow-sensitive typing
Another really neat feature of Whiley is its totally novel typing paradigm, flow-sensitive typing, which it introduced to the world in 2009. Whiley completely rewrites Java's typing model for ease of use, and to fulfill its goal of verifiability. First, record types are introduced:
type Circle is { int x, int y, int radius }
type Square is { int x, int y, int dimension }
type Rectangle is { int x, int y, int width, int height }
Record types have been proposed for inclusion in upcoming versions of Java, but are still only a preview feature in Java 14. Record types in Java would provide -- within an extremely compact definition -- a class definition with a default constructor, public instance variables based on the provided parameter names, and getters and setters for those parameters.
The above Whiley code is all that is needed to define three classes, with constructors and public instance variables. In addition, with Whiley, instance variables can be accessed and modified with the simpler dot notation, rather than using verbose setters and getters:
Circle c1 = {x: 10, y: 20, radius: 30}
c1.x = 20
Whiley also provides union types:
type Shape is Circle | Square | Rectangle
In the code above, Shape
is a union type of Circle
, Square
, and Rectangle
. The "pipes" |
work in a very similar manner to the boolean OR operator from Java |
-- an object is a Shape
if it conforms to at least one of the three classes Circle
, Square
, or Rectangle
.
Flow-sensitive typing means that the following is valid Whiley code:
function area(Shape s) -> int:
if s is Circle:
return (3 * s.radius) * s.radius
else if s is Square:
return s.dimension * s.dimension
else:
return s.width * s.height
The is
operator, above, checks during runtime if the object s
conforms to the Circle
or Square
record definition. If the former, then we can simply use Circle
-specific methods and accessors, without needing to explicitly cast s
to a Circle
object, like in Java. Since Whiley is aware of the definition of Circle
, it can check that all operations performed using s
after the if s is Circle
clause are valid, according to the definition of Circle
.
N.B.: Whiley does not yet support floating-point numeric types, hence the incorrect circle area formula above. I assume this is due to the inherent granularity of floating-point types on digital computers and some difficulty with reasoning about the "correctness" of a program where machine epsilons are important.
The above is accomplished through intersection types. Whiley takes the type of object s
, whatever that happens to be, and intersects it with type Circle
. Since s
must be a Shape
, there are three possibilities:
Circle & Circle
Square & Circle
Rectangle & Circle
Whiley's compiler recognizes that the last two type intersections are the null set (because Circle
doesn't extend either Square
or Rectangle
and vice versa), so the if
statement will only execute if s
is a Circle
record. A more interesting version of this might be something like:
import string from std::ascii
import std::io
method main():
integerOrString(10)
integerOrString("10")
method integerOrString(int|string x):
if x is int:
io::println(x+5)
else:
io::println(x)
Whiley uses flow-sensitive typing to pick the if
or else
branch at runtime. If x
is an int
, it will be increased by 5, then printed. Otherwise, it will be printed as-is. This, of course, works similarly to instanceof
in Java, but is much more elegant.
#10. Jython & JRuby -- language interoperability
Jython and JRuby are two of the earliest non-Java JVM languages, first released in 1997 and 2001, respectively. These languages are designed to offer interoperability between Java and Python, and Java and Ruby, respectfully. This means that Jython allows for Python code to be run from within Java, and vice versa. JRuby provides the same features, but for the Ruby language.
Here's an example of some simple Python code being run within Java using Jython:
import org.python.util.PythonInterpreter;
public class JythonHelloWorld {
public static void main (String[] args) {
try(PythonInterpreter pyInterp = new PythonInterpreter()) {
pyInterp.exec("print('Hello, World!')");
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
...similar code, but for Ruby running with JRuby:
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
public class JRubyHelloWorld {
ScriptEngineManager mgr = new ScriptEngineManager();
ScriptEngine rbEngine = mgr.getEngineByExtension("rb");
public static void main (String[] args) {
try {
rbEngine.eval("puts 'Hello, World!'");
} catch (ScriptException ex) {
ex.printStackTrace();
}
}
}
Language interoperability is a bit of a hot topic in the JVM community at the moment, especially with the recently-released GraalVM Java Virtual Machine and Development Kit. GraalVM offers mutual interoperability between multiple languages, including:
- JavaScript and Node.js
- Ruby and Ruby on Rails
- R
- Python 3
- LLVM-based languages
Will GraalVM -- an Oracle product -- spell the end for Jython and JRuby? Will Oracle peel developers away from these other projects to work on GraalVM? Will I think of a third example? We'll just have to wait and see.
#11. Clojure -- "LISP for the JVM"
While Frege and Eta provide slightly altered versions of Haskell for the JVM, Jython and JRuby offer interoperability with Python and Ruby, respectively, and GraalVM expands that interoperability to include JavaScript, R, and more, Clojure brings yet another language to the JVM: LISP.
I've developed an infatuation with LISP recently. I like the fact that -- like Ioke, above -- it's a homoiconic language, meaning the program itself is a data structure within the programming language, namely a list. This property means that LISP programs can operate on their own source code as though they were data (a list), because they are. Input, output, and source code all conform to the same structure, and can easily be read, written, and manipulated using the LISP language.
Clojure brings this flexibility to the JVM, while refreshing and updating LISP for the twenty-first century. Clojure takes full advantage of the JVM, as well, allowing Java methods and classes to be called from within Clojure code:
(doto (java.util.HashMap.)
(.put "apple" 1)
(.put "banana" 2))
The above code returns a java.util.HashMap
; in Clojure syntax, this will be written as {"banana" 2, "apple" 1}
. Threading macros in Clojure also allow for easy functional programming:
(->> (range 10)
(map inc)
(filter even?))
The above code returns the list (2 4 6 8 10)
.
With all of these amazing projects providing interoperability between Java and various other programming languages, I think the JVM will be around for a long, long time.
#12. Scala -- "everything is an object"
As we get toward the end of this list, we're now in the region of "popular" non-Java JVM languages. Scala, Kotlin, and Groovy are the only three languages on this list which crack the top 25 most popular languages on GitHub. As Scala is a language I am relatively familiar with, let's start there.
Scala is designed to be a Scalable Language, great for short scripts, small projects, gigantic monolithic services, and agile microservices alike. Scala is the brainchild of Professor Martin Odersky of l'École Polytechnique Fédérale de Lausanne (EPFL), who has helped to develop several JVM languages, including Pizza and Generic Java (which became the basis of generic types in Java), as well as implementing the GJ compiler for Java, on which javac was then based.
Scala sits at the intersection of several popular paradigms and trends in programming. The first, and probably most obvious of these, is object-oriented programming. As a JVM language, Scala inherits and builds upon Java's object hierarchy.
While Java has always had a sort of two-worlds approach to OOP, with "primitives" treated differently than true Object
s, Scala unifies the hierarchy by promoting these basic data types to "real" objects -- int
becomes Int
, double
becomes Double
, and so on. These value types all descend from a common value object, AnyVal
.
"But what about boxing and unboxing?" you might ask, "...don't they effectively convert primitives to objects and vice-versa?" While this is true, this conversion must be explicitly performed if you want to, say, call a method on a Java primitive:
jshell> 3.toString()
| Error:
| ';' expected
| 3.toString()
| ^
jshell> int x = 3
x ==> 3
jshell> x.toString()
| Error:
| int cannot be dereferenced
| x.toString()
| ^--------^
jshell> (new Integer(3)).toString()
$2 ==> "3"
Scala implicitly makes these conversions for you (see #15), so all of Integer
's methods are available to use on integer literals:
scala> 3.toString()
res0: String = 3
And since the Scala compiler converts these to Java primitives at compile time, there is no performance overhead. An additional benefit of this is that no special operators need to be defined for primitives. The arithmetic operators +
, -
, *
, /
(and more) are simply defined as functions within Scala, which allows all sorts of symbols within identifiers.
Java's Project Valhalla aims to bring Scala-like value types to Java via generic specializations. Valhalla also introduces "value types" for Java which are different from Scala's value types, and closer to C's
struct
s.
Similarly, on the "Java-like object" side of the inheritance tree, we have AnyRef
, which is the ancestor of all reference types. AnyVal
and AnyRef
both descend from Any
, Scala's equivalent of Java's Object
root class. Specifying Any
in a type parameter means that any data type can be substituted, even basic data types like Int
and Double
.
Similarly, at the bottom of the hierarchy, we have the Null
and Nothing
types. The Null
singleton object can be substituted for any class which extends AnyRef
, and Nothing
can be substituted for any class at all. These "pinch points" at the bottom and top of the class hierarchy unify Scala's object model in a way which makes Java's look a bit fragmented and incomplete.
There are other, less obvious manifestations of the "everything is an object" philosophy in Scala. These are achieved primarily through syntactic sugar, which interprets the name of an object (or a companion object of a class*) followed by a parameter list in parentheses as calling the apply()
method on that object:
scala> :paste
// Entering paste mode (ctrl-D to finish)
object Example {
def apply(x: Int) { println(s"x is $x") }
}
// Exiting paste mode, now interpreting.
defined object Example
scala> Example(4) // equivalent to Example.apply(4)
x is 4
* Objects defined with the
object
keyword in Scala are equivalent to singleton classes in Java. And a companion object can be thought of as a container forstatic
-like methods, which can be called by any object of that class. Scala companion objects must have the same name as their companion class. (The confusion betweenObject
s,object
s and objects is real.)
This syntactic sugar means that we can have classes that code like classes but act like functions, including:
actual
Function
s -- which are objects thatextend AnyRef
and have anapply()
methodMap
s -- whoseapply()
functions take a key parameter and return a valueList
s and other sequences -- whoseapply()
methods take an index and return the value at that index
These unifications of the object hierarchy and syntax mean that there are fewer "exceptions to the rules" in Scala. You don't access array elements with []
like you do in Java; Function
s aren't a special type of construct, separate from objects; and there is no primitive/object distinction. Scala adds a bit of syntactic sugar, and extends the object hierarchy a bit, and everything seems to nicely fall into place.
#13. Scala -- everything is an expression
Scala attempts to remove even more "exceptions to the rules" with the idea that "everything is an expression". In most programming languages, there are distinctions between various kinds of syntactical constructs, like expressions, operators, and control structures. As mentioned above, Scala eliminates the need for explicit, hard-coded operators through its expanded class hierarchy, but it also streamlines many control structures by applying a few concepts from the functional programming paradigm.
In many languages, if-else
is a control structure which looks something like
if (condition) {
// do something
} else {
// do something different
}
Within the if
and else
blocks, you can affect the execution of the program through side-effects, by mutating variables or performing IO, etc. If we wanted a variable to depend on the value of the condition, we would do something like
var y;
if (condition) {
y = ...
} else {
y = ...
}
This requires us to be able to mutate the value of y
(ie. it's not constant, which can be undesirable), and it requires us to "reach up" out of the scope of the if-else
block to affect the value of y
in the enclosing scope.
In many languages, you cannot do something like this, though:
var y = if (x < 3) "less then" else "greater than or equal to"
...that is, assigning the result of the if-else
to a variable. This requires a bit of a re-orientation from procedural to functional programming. In procedural programming, we have explicit return
statements, and often "return early" for performance and code cleanliness reasons. Multiple and early returns are frowned upon in functional programming, however, because it makes it more difficult for the compiler to reason about the flow within the program.
One common trope in functional languages is the idea that the last-computed value in a block is the "return value" of that block. This allows us to omit the return
keyword entirely, and can simplify the flow of the code considerably. For example:
val area = {
val pi = 3.14159
val r = 10
pi * r * r
}
The lack of return
statements means that this block will always run from start to finish. Because we're assigning to a variable, the expression isn't parameterized in any way, and the value of the block will always be the same. If this were a function, we could cache the result to avoid recomputing the same value over and over.
The "last computed value is the return value" concept means that even though the last line of the block (pi * r * r
) wasn't assigned to any variable within the block, because it's the last computed value in that block, it is the value of the block. Since the block can be evaluated, it can be assigned to a variable, as is done above.
if-else
statements in Scala follow these same rules. The last computed value in a block is the value of that block, so while in Java, the following code
if (x < 3) {
"less than"
} else {
"greater than or equal to"
}
...would have no effect, in Scala, the entire if-else
block assumes a value after running, which means it can be assigned to a variable
var y = if (x < 3) {
"less than"
} else {
"greater than or equal to"
}
And just like in Java, we can drop the {}
around single-line if-else
blocks:
var y = if (x < 3)
"less than"
else
"greater than or equal to"
// or just
var y = if (x < 3) "less than" else "greater than or equal to"
Treating even basic control statements as expressions which "return" a value lets us streamline our code and makes it easier to reason about and test program execution.
As a side note, we can kind of recreate this behavior using only "Java-style"
if
s andreturn
s by using implicit conversions and by-name parameters:scala> :paste // Entering paste mode (ctrl-D to finish) implicit class Elseable (x: Any) { def ELSE (elval: => Any): Any = if (x == ()) return(elval) else return(x) } def IF(cond: Boolean)(ifval: => Any): Any = if (cond) return(ifval) // Exiting paste mode, now interpreting. defined class Elseable IF: (cond: Boolean)(ifval: => Any)Any scala> IF (true) { "T" } ELSE { "F" } res0: Any = T scala> IF (false) { "F" } ELSE { "T" } res1: Any = T scala> IF (true) { "T" } res2: Any = T scala> IF (false) { "F" } res3: Any = ()
#14. Scala -- case classes and pattern matching
Pattern matching is like regular expressions, but for the structure of objects rather than for the structure of strings. Pattern matching can be found throughout professional Scala code, but one of the more obvious places it can be seen is in match
expressions.
Think of a match
expression like a superpowered switch
. Whereas in Java, we define a switch
with contains multiple case
s, separated by break
s, in Scala, we define a match
expression with multiple case
s, but the compiler inserts breaks for us whenever it encounters a new case
keyword. This means there is no fall-through in Scala match
expressions.
A simple, Java-like match
expression might look like:
scala> :paste
// Entering paste mode (ctrl-D to finish)
val x = 3
x match {
case 1 => "x is one"
case 2 => "x is two"
case 3 => "x is three"
}
// Exiting paste mode, now interpreting.
x: Int = 3
res4: String = x is three
...but that doesn't even scratch the surface of what can be done with Scala match
expressions. You can add filters in your case
s (and default matches, denoted by case _ =>
):
scala> :paste
// Entering paste mode (ctrl-D to finish)
val x = 3
x match {
case _: Int if x < 0 => "x is negative"
case _: Int if x > 0 => "x is positive"
case _ => "x is zero" // default
}
// Exiting paste mode, now interpreting.
x: Int = 3
res11: String = x is positive
You can match on specific types:
scala> def whatIsIt (x: Any) = x match {
| case _: Double => "x is a Double"
| case _: Int => "x is an Int"
| case _: String => "x is a String"
| }
whatIsIt: (x: Any)String
scala> whatIsIt(3)
res8: String = x is an Int
scala> whatIsIt(3.4)
res9: String = x is a Double
scala> whatIsIt("3")
res10: String = x is a String
You can even use pattern matching to deconstruct objects, pulling out the arguments that were used to create them:
scala> :paste
// Entering paste mode (ctrl-D to finish)
sealed trait Shape
case class Rect(height: Int, width: Int) extends Shape
case class Circle(radius: Int) extends Shape
case class Square(size: Int) extends Shape
// Exiting paste mode, now interpreting.
defined trait Shape
defined class Rect
defined class Circle
defined class Square
scala> def whatIsIt (s: Shape) = s match {
| case Rect(h, w) => s"a Rect with width $w and height $h"
| case Circle(r) => s"a Circle with radius $r"
| case Square(s) => s"a Square with side length $s"
| }
whatIsIt: (s: Shape)String
scala> val r = Rect(3, 4)
r: Rect = Rect(3,4)
scala> val c = Circle(5)
c: Circle = Circle(5)
scala> val s = Square(9)
s: Square = Square(9)
scala> whatIsIt(r)
res15: String = a Rect with width 4 and height 3
scala> whatIsIt(c)
res16: String = a Circle with radius 5
scala> whatIsIt(s)
res17: String = a Square with side length 9
In that last example, I made use of a sealed trait
which was extended by several case class
es. Scala's trait
s are similar to Java's interface
s, and case class
es are similar to Whiley's record types.
match
expressions and case class
es are truly a match made in heaven [hold for applause]. Why? Well, because by using the type of the object to control the flow of the program, the compiler is helping to check if your code is correct.
A sealed trait
, as used above, means that any classes which extend it must appear in the same location (usually the same source code file) in which the sealed trait
is defined. This means that in our example, Rect
, Circle
, and Square
are the only classes which extend Shape
, so whenever we match
on a Shape
, the compiler can double-check that we've covered all our bases by providing patterns for Rect
, Circle
, and Shape
, and only those three classes.
We will get a compile time error / warning if we have a non-exhaustive match
(if we missed a case class
) or if we try to match on a class which s: Shape
could never match (a Double
, for instance). match
expressions and case class
es are a really powerful duo in Scala.
#15. Scala -- implicit programming
One of the guiding principles of the Python programming language (the Zen of Python) is the phrase "explicit is better than implicit". That quote, from Tim Peters, a major early contributor to Python and inventor of the Timsort sorting algorithm, must not have made it to Martin Odersky's ears before he started work on Scala just a few short years later. Scala is rife with implicits.
To be fair, Scala was built on top of Java, and to extend Java in any meaningful way -- for instance, to add functionality to a class defined in Java source code -- you need to jump through some hoops. This is made even more difficult for classes like String
, which is immutable and final
(for good reasons). Scala uses a concept called implicit conversions to automatically convert Java classes into Scala classes (and vice versa), covertly jumping through those hoops for you.
If you've programmed in Java before, you've actually used implicit conversions already. When you write something like
jshell> 1 + 3.0
$1 ==> 4.0
...Java is converting the int 1
to a double 1.0
, automatically and implicitly (nothing in the code tells you outright that that is what's happening). But integer and floating-point numbers have very different internal representations, so it's not just a matter of relabeling an int
as a double
, the compiler actually needs to shift bits around.
As we know from Java, even explicit primitive conversions can be dangerous. If you try to convert a long
to an int
, you'd better be sure that that long
doesn't exceed Integer.MAX_VALUE
jshell> long l = Integer.MAX_VALUE + 2000000000L
l ==> 4147483647
jshell> int i = l
| Error:
| incompatible types: possible lossy conversion from long to int
| int i = l;
| ^
jshell> int i = (int) l
i ==> -147483649
Java will only implicitly widen primitive types. To narrow them (double
-> float
or long
-> int
-> char
-> byte
), you must explicitly declare the conversion with a typecast, as above. But this can lead to unexpected results, like the long
above that wrapped around to a negative int
.
To avoid overly verbose conversions between Java and Scala types, Scala uses implicit conversions to convert Java's primitives to Scala's wrapper types (which extend AnyVal
), Java's String
s to string-like helper classes in Scala, and more.
But this can be just as dangerous in Scala as it is in Java, so be careful!
Implicit programming comes in different shapes and sizes in Scala, but the two most common usages are
-
implicit
parameters, and -
implicit
conversions
Implicit parameters in Scala are arguments which are not explicitly sent to a function. These parameters must be declared implicit
in the parameter list, and there must be an implicit
variable of the same type within scope (defined by some slightly complex scoping rules):
scala> implicit val ii: Int = 42
ii: Int = 42
scala> def answer (to: String)(implicit num: Int) {
| println(s"The answer to $to is... $num!")
| }
answer: (to: String)(implicit num: Int)Unit
scala> answer("life, the universe, and everything")
The answer to life, the universe, and everything is... 42!
Notice how, in the above code snippet, we didn't explicitly send the value 42
to the procedure answer
. Instead, it knew it needed an implicit Int
, and it looked for -- and found one -- within scope. There are rules for handling collisions between implicit
parameters, passing multiple implicit
arguments, and making implicit
parameters explicit with the implicitly
keyword.
implicit
parameters are usually used when you have many methods passing "environment"-type arguments to each other. Rather than clog up your code with lots of argument-passing, make everything implicit!
scala> implicit val bb = false
bb: Boolean = false
scala> implicit val dd = 19.0
dd: Double = 19.0
scala> implicit val ss = "my string"
ss: String = my string
scala> def myBool(implicit myb: Boolean) {
| println(s" my boolean is $myb")
| }
myBool: (implicit myb: Boolean)Unit
scala> def myDub(implicit myd: Double) {
| println(s" my double is $myd")
| }
myDub: (implicit myd: Double)Unit
scala> def myStr(implicit mys: String) {
| println(s" my string is $mys")
| }
myStr: (implicit mys: String)Unit
scala> def myThings() {
| println("These are my things:")
| myBool
| myDub
| myStr
| }
myThings: ()Unit
scala> myThings
These are my things:
my boolean is false
my double is 19.0
my string is my string
Implicit conversions are slightly more complex (and slightly more dangerous). They are disabled by default, but can be enabled with import scala.language.implicitConversions
.
With the above package imported, when an object in Scala tries to access a method or value which it wouldn't normally have access to, the Scala compiler will look (following the same implicit
scoping rules) for an implicit def
which takes a single argument of that object's type, and returns an object of any type which has that missing method or value defined:
scala> "hey".exclaim()
<console>:12: error: value exclaim is not a member of String
"hey".exclaim()
^
scala> import scala.language.implicitConversions
import scala.language.implicitConversions
scala> class Exclaimable (s: String) {
| def exclaim(): String = s + "!"
| }
defined class Exclaimable
scala> implicit def string2Exclaimable (s: String): Exclaimable = new Exclaimable(s)
string2Exclaimable: (s: String)Exclaimable
scala> "hey".exclaim()
res1: String = hey!
Implicit conversions seem great at first glance (and they're used heavily by the language itself to convert Java <=> Scala), but -- I cannot stress this enough -- they are dangerous. Since Scala 2.10, implicit conversions are discouraged, and should be replaced with explicit converters or implicit class
es instead:
scala> "hey".exclaim()
<console>:12: error: value exclaim is not a member of String
"hey".exclaim()
^
scala> implicit class Exclaimable (s: String) {
| def exclaim(): String = s + "!"
| }
defined class Exclaimable
scala> "hey".exclaim()
res1: String = hey!
#16. Groovy -- partial application and composition of functions
Apache's Groovy programming language offers lots of neat ways of working with closures, defined in Groovy as "open, anonymous, block[s] of code that can take arguments, return a value and be assigned to a variable".
Groovy's closures take the form
{ [closureParameters -> ] statements }
They are curly-brace-delimited blocks of code, with zero or more comma-separated parameters transformed into some output. They may return a value, cause side-effects like printing to the terminal, both, or neither. They can access variables outside the scope of their defining block as well. The following are all valid closures in Groovy:
c0 = { x++ }
c1 = { x, y -> x + y }
c2 = { -> println("sup") }
c3 = { a, b, c -> }
Closures can be called with a comma-separated list of parameters just like normal functions, so the above closures could be used like:
[in]> c1(3, 4)
[ou]> 7
[in]> x = 42
[ou]> 42
[in]> c0()
[ou]> 43
[in]> c1(-3, 6)
[ou]> 3
[in]> c2()
sup
[ou]> null
[in]> c3(19, "popty-ping", false)
[ou]> null
Groovy lets you play with closures in interesting ways. For instance, Groovy allows partial application of closures -- which it calls "currying", acknowledging that this is not actually currying in the strict sense:
[in]> x_pow_y = { x, y -> x**y }
[ou]> groovysh_evaluate$_run_closure1@15130002
[in]> x_pow_y(2, 3)
[ou]> 8
[in]> x_pow_3 = x_pow_y.rcurry(3)
[ou]> org.codehaus.groovy.runtime.CurriedClosure@1f6f0fe2
[in]> x_pow_3(2)
[ou]> 8
[in]> two_pow_y = x_pow_y.curry(2)
[ou]> org.codehaus.groovy.runtime.CurriedClosure@2591d23a
[in]> two_pow_y(3)
[ou]> 8
As seen above, we can send only the leftmost parameter (with curry()
) or only the rightmost parameter (with rcurry()
) and get back a new function which takes one fewer arguments. (Here's a good explanation of the differences between currying and partial application if you're interested.)
You can also set an arbitrary parameter with ncurry
:
[in]> sum_abc = { a, b, c -> a + b + c }
[ou]> groovysh_evaluate$_run_closure1@26d028f7
[in]> sum_abc(2, 3, 4)
[ou]> 9
[in]> sum_a3c = sum_abc.ncurry(1, 3)
[ou]> org.codehaus.groovy.runtime.CurriedClosure@45c90a05
[in]> sum_a3c(2, 4)
[ou]> 9
Groovy offers lots of other cool features for closures, including trampolining for tail-call optimization, memoization, and transforming regular functions into closures via method pointers, but one other small feature I want to take a look at here is function composition.
You've probably encountered function composition in your high school mathematics classes, or when using the pipe |
operator in a terminal. Basically, a composition of functions works like the following:
(f << g)(x) == f(g(x))
In Groovy, <<
is the function composition operator, and it pipes the output of function g
into the input of function f
:
[in]> f = { x -> x + 2 }
[ou]> groovysh_evaluate$_run_closure1@5db9f51f
[in]> g = { x -> x * 3 }
[ou]> groovysh_evaluate$_run_closure1@12f8682a
[in]> (f << g)(5)
[ou]> 17
[in]> f(g(5))
[ou]> 17
You can also compose functions in reverse order with >>
:
[in]> (f >> g)(5)
[ou]> 21
[in]> g(f(5))
[ou]> 21
Function composition makes it easy to chain functions together, creating reusable data manipulation pipelines. Plus, it's neat!
#17. Groovy -- easy regular expressions
In my opinion, one of the places where Groovy most obviously outshines Java is in its support for regular expressions. It's not even a competition. Groovy has all sorts of extra bits of syntax to help you define and use regex.
If you're new to regex, or want a refresher, why not check out my interactive guide 20 Small Steps to Become a Regex Master
In Java, defining and using a regular expression is -- like most things Java -- very verbose:
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Example {
public void run() {
String example = "The President of the United States of America";
Pattern pattern = Pattern.compile("\\b[a-zA-Z]{1,3}\\b");
Matcher matcher = pattern.matcher(example);
while (matcher.find())
System.out.println(matcher.group());
}
}
Running the above code in the jshell
would give:
jshell> (new Example()).run()
The
of
the
of
There's a lot of ceremony for setting up and running a regular expression over a Java String. (And I made the example about as short as I could.) In Groovy, it's much simpler:
example = "The President of the United States of America"
pattern = ~/\b[a-zA-Z]{1,3}\b/
matcher = example =~ pattern
We can then find all matches with
[in]> matcher[0..-1]
[ou]> [groovier, better]
How simple is that? Above, we defined pattern
using a slashy string, which is a string surrounded by //
rather than by ""
. Slashy strings are raw strings, in which escape sequences are ignored, so we don't need to "double-up" on the backslashes to get the regex word boundary sequence (just \b
instead of \\b
).
We also used the pattern operator ~
before the string, which transforms that string into a Pattern
in a manner similar to Pattern.compile()
. Then, we ran the expression on our example string with the find operator =~
, instead of running pattern.matcher()
as we did in Java.
In Groovy, you can also use the presence or absence of a match as a booleean value...
[in]> if ("The password is... PorkChop" =~ /(?i)porkchop/) {
[in]> println "you're in"
[in]> } else {
[in]> println "scram!"
[in]> }
you're in
...easily extract and assign several variables at once using Groovy's multiple assignment...
[in]> (ghost1, ghost2, ghost3, ghost4) = ("Inky, Pinky, Blinky, and Clyde" =~ /[A-Z][a-z]*/)
[ou]> java.util.regex.Matcher[pattern=[A-Z][a-z]* region=0,30 lastmatch=Clyde]
[in]> println "Oh no, it's $ghost2, $ghost1, $ghost4, and uh... $ghost3?"
Oh no, it's Pinky, Inky, Clyde, and uh... Blinky?
...and much more! By making regular expressions less verbose, Groovy is making them more accessible, and less anxiety-inducing for new devs.
#18. Groovy -- JSON-to-classes
Love it or hate it, JSON is quickly becoming the de-facto standard for data transfer on the web. This simple text file format, which describes arbitrarily-nested objects of basic data types and arrays has quickly achieved widespread appeal because it is easy to write, easy to read, and easy to parse.
Although inspired by -- and named after -- the language, JavaScript Object Notation (JSON) was not strictly a subset of JavaScript until 2019, when a new version of ECMAScript was released. (JSON used to allow characters which were illegal in JavaScript objects, in particular.) JSON objects can now be quickly, easily, and safely parsed from text files and automatically translated into objects in JavaScript.
Groovy provides the same functionality, but on the JVM. Groovy's built-in JsonSlurper
class allows JSON data to be automatically parsed from a simple string format into a fully-fledged Groovy object:
[in]> slurper = new groovy.json.JsonSlurper()
[ou]> groovy.json.JsonSlurper@bea5941
[in]> result = slurper.parseText('{"person":{"name":"Guillaume","age":33,"pets":["dog","cat"]}}')
[ou]> [person:[name:Guillaume, age:33, pets:[dog, cat]]]
[in]> result.person.age
[ou]> 33
[in]> result.person.pets
[ou]> [dog, cat]
[in]> result.person.name
[ou]> Guillaume
Of course, it's possible to do this in plain Java, but as JSON is becoming more and more popular, there's more and more of an argument to be made for including it in a language's standard library, which Groovy has already done! And it "just works", right out of the box -- super easy!
#19. Kotlin -- JavaScript transpilation
While we're on the topic of the web, it can sometimes be difficult for backend developers to showcase their work, or to easily create portfolios. Frontend devs have a huge canvas at their disposal -- the web browser -- which can be really difficult to access if you're not working in JavaScript or TypeScript.
Kotlin tries to close the frontend / backend dev divide a bit with its ability to compile not just to Java bytecode via the JVM, or to over a dozen different native architectures through LLVM, but also by transpiling directly to JavaScript, where you can run Kotlin in your browser!
Just write a simple Kotlin function like
import kotlin.browser.*
import kotlin.dom.*
fun main(args: Array<String>) {
document.getElementById("tooltip")?.appendText("The first step")
}
...transpile, and you'll get JavaScript output that looks something like:
if (typeof kotlin === 'undefined') {
throw new Error("Error loading module 'KotlinFunWeb'. Its dependency 'kotlin' was not found. Please, check whether 'kotlin' is loaded prior to 'KotlinFunWeb'.");
}
var KotlinFunWeb = function (_, Kotlin) {
'use strict';
var appendText = Kotlin.kotlin.dom.appendText_46n0ku$;
function main(args) {
var tmp$;
(tmp$ = document.getElementById('tooltip')) != null ? appendText(tmp$, 'The first step') : null;
}
_.main_kand9s$ = main;
main([]);
Kotlin.defineModule('KotlinFunWeb', _);
return _;
}(typeof KotlinFunWeb === 'undefined' ? {} : KotlinFunWeb, kotlin);
Showing off your Kotlin skills has never been easier! Here's a template to get you started, and some larger examples (of a password manager, a digital bookshelf, and a tracker of people currently in outer space) for inspiration!
#20. Kotlin -- coroutines
Concurrent programming in Java is a terrifying thing for a newbie. When should I use atomics? And volatile
s? And should I synchronize
on this class? Or this block? Or this
?
At best, you can end up with an application which isn't currently crashing or deadlocking but which could break unexpectedly at any moment, without any easy way to even trace what went wrong. At worst, you could end up with silent race conditions that corrupt your data without alerting you to any problems at all!
Kotlin tries to ease this pain a bit with its coroutines. Coroutines were proposed in the late 1950s as a sort of counterpart to subroutines, instead of a parent-child relationship, where control passes from the (more central) parent to the (more peripheral) child, then back to the parent, coroutines introduce a symmetric relationship. A given coroutine can yield
to another at any time. A simple (pseudocode) coroutine might look like
var q := new queue
coroutine produce
loop
while q is not full
create some new items
add the items to q
yield to consume
coroutine consume
loop
while q is not empty
remove some items from q
use the items
yield to produce
In the above example, the produce
coroutine fills the queue
until there's no more space, then yield
s to consume
, which empties the queue. These two couroutines bounce back and forth, yielding the program flow.
The main advantage of coroutines is that they're asynchronous. In the above example, the produce
coroutine isn't "waiting" or "sleeping" while the consume
coroutine is running, it's simply stopped. It's not using any resources. These two coroutines could be running on different threads or on the same thread.
Because coroutines and OS threads don't have a 1:1 relationship, coroutines are very lightweight. While your system might begin to balk if you try to create more than a few thousand -- or even a few hundred -- threads, you can easily create millions of Kotlin coroutines at once. Coroutines allow for an easy parallel, asynchronous division of labor spread evenly among your computer's resources.
To learn more about different approaches to concurrent programming in Kotlin, check out this SO post on the differences between threads and coroutines, and this short blog post which compares and contrasts actors and coroutines.
Do you know of any other cool features I neglected to mention above? Are any of the above features also available in your language of choice? Let me know in the comments!
People who viewed the above article also viewed:
- me soliciting $ from Internet strangers
- acid-washed Facebook
- my other Dev.To articles
- 𝐭𝓲Ⓜ𝑒ⓒυβε
Thanks for reading!
Top comments (3)
wow! nice post. I didn't get time to read it all, but I like your header image, and the code snippets.
personally, java is my favorite language and I have to get back this post as soon as possible. Anyway congrats and keep the posts coming. Cheers
Cool information also find my observations on coroutines androidcoding.in/2020/05/07/androi...
nice post, only thing I would mention is that kotlin coroutines are more like continuations than coroutines themselves