Lessons:
- Characterizing Kotlin programming environment:
- Statically typed
- For JVM and for native and cross-platform
- Multiparadigm: imperative, object-oriented and functional.
- Programming languages retrospective and comparison with Kotlin.
- Since functional programming languages to object oriented, Lisp, Java, JavaScript, C#, Scala, etc
- Software and source code quality criteria: Reliability, Testability, Readability and Extensibility.
- Lessons approach:
- Git repository
- Multi-module project (one module per lesson) with gradle wrapper (version 7.5.1)
- Kotlin release 1.6....
- IntelliJ 2022
- Homework workouts
-
Interface as a contract (e.g.
Stack<T>
) with abstract member functions
-
Abstract classes => cannot be instantiated
- Instantiate => create an instance of a class
- Object's state => values of the object's properties
- Restricting and managing access to object’s state (i.e. instance properties or fields).
- Hide properties inside the class (
private
)
- Provide methods/functions to access object’s state:
- e.g.
NaifDate
member functions nextMonth()
and addDays(inc)
- Limit Mutability
- Kotlin mutable properties (
var
) versus immutable (val
)
- Implement
Stack
interface based on auxiliary class Node<T>(val item: T, val next: Node<T>?)
with the expected behavior:
interface Stack<T> {
fun push(item: T)
fun peek(): T
fun isEmpty(): Boolean
fun pop(): T
}
|
fun main() {
val stk = StackImpl<String>()
stk.push("ISEL")
stk.push("TDS")
println(stk.peek()) // TDS
while( !stk.isEmpty() ) println( stk.pop() ) // TDS ISEL
stk.peek() // throws NoSuchElementException
}
|
- "isolate each part of the program and show that the individual parts are correct"
- E.g. refactoring the former
main()
in individual unit tests:
new instance of Stack should be empty
last pushed item is the peeked item
after pop of a Stack with single item it stays empty
pop an empty stack throws NoSuchElementException
peek an empty stack throws NoSuchElementException
- Unit testing frameworks (e.g. JUnit, TestNG, etc) provide:
- Strict, written contract, through annotations (e.g.
@Test
, @Expect
, etc).
- API to express and evaluate the expected behavior (i.e. assertions).
- Automated runtime to scan, perform and collect tests results.
- E.g.
kotlin.test.
API:
assertEquals(expected, actual)
assertTrue(condition)
assertFailsWith<ExceptionClass> { ... /* block */ ... }
- Limit Mutability, E.g. Implementation of an immutable
FixedStack
- Implementation of an immutable
NaifDate
- Classes and data classes
- Classes may have a primary constructor and one or more secondary constructors.
- The primary constructor is a part of the class header.
- Initialization code can be placed in initializer blocks prefixed with the
init
keyword.
-
Any
- the root of the Kotlin class hierarchy
- Canonical functions:
equals
, hashCode
e toString
- Modifiers:
abstract
, open
, and override
- E.g.
override fun toString(): String = "$day-$month-$year"
- Classes and data classes (provides implementation of canonical methods based on its properties)
- Inspecting resulting data classes with
javap
(e.g. toString()
and equals()
)
- Equality - Structural equality (
==
a check for equals()
)
- Referential equality (
===
two references point to the same object)
- Properties with custom accessors - called every time you access the property (this way you can implement a computed property)
- Property versus Custom getter:
-
val nextMonth = month % 12 + 1
=> private final int nestMonth;
(JVM field)
-
val nextMonth get() = month % 12 + 1
=> private final int getNextMonth();
(JVM function)
- Implement a prefix calculator for integer expressions.
- Object oriented approach =>
interface IntExpr { fun eval() : Int }
-
IntExpr
<|-----
IntExprLiteral
-
IntExpr
<|-----
IntExprOperator
- v1 – without typed operator - Operator is a
Char
- v2 – with a typed operator (i.e. Strongly Typed)
- Enumerated Type (e.g. Kotlin
enum class
):
- A data type with a set of named values, i.e. constants (e.g.
SUM
, DIV
, etc)
- Each constant is an instance of its enumerated type (e.g.
Operator
)
-
Companion Object - its members can be called simply by using the class name as the qualifier (e.g.
Operator.parse(...)
)
- E.g.
enum class Operator(private val opr: Char) {
...
companion object {
fun parse(opr: Char) : Operator {
return values().find { it.opr == opr } ?: throw IllegalArgumentException()
}
}
}
- Object declarations -
object
- singleton pattern.
- Operator overloading, e.g. binary operations
- Association
--->
versus Inheritance -----|>
- UML classes diagrams.
- E.g.
NaifDate
with Month
and IntExprOperator
with Operator
-
Sealed classes and interfaces represent restricted class hierarchies that provide more control over inheritance
-
sealed
versus open
- Implementing functional approach for
IntExpr
-
Dynamic dispatch (polymorphism) versus Pattern matching
- E.g. dynamic dispatch -
invokeinterface
- E.g. pattern matching
fun IntExpr.eval(): Int = when(this) {
is IntExprLiteral -> nr
is IntExprOperator -> opr.eval(a.eval(), b.eval())
}
- Structuring modules to build a TicTacToe game for UI console and GUI (graphical user interface):
tictactoe-model
-
ui
– console single-device
-
storage
- persistence in Document Database - NoSQL | MongoDB
-
ui
– console between devices
-
gui
- graphical user interface
-
tictactoe-model
: Player
, Position
, Move
and Board
- Control illegal arguments and states:
-
IllegalArgumentException
and IllegalStateException
- runtime checks —
require()
and check()
-
IllegalArgumentException
and IllegalStateException
- Manage
Position
instances
private constructor
- Make
Position(..., ...)
forward to:
companion object { operator fun invoke(l: Int, c: Int): Position {...}}
- Parentheses are translated to calls to
invoke
with appropriate number of arguments.
- Manage invalid states
- Type checks and casts
- "Unsafe" cast operator, E.g.
(board as BoardWin).winner
-
lesson10-tictactoe-ui
module depending of lesson09-tictactoe-model
-
lesson10-tictactoe-ui\build.gradle.kts
contains:
-
dependencies { implementation(project(":lesson09-tictactoe-model"))
... }
-
Command processing loop (i.e.
readCommandsOop
):
- Read console input arguments
- Parse arguments and find corresponding command
- Execute command and show:
- the new State (i.e.
Board
) on success
- error message if failed.
fun readCommandsOop(cmds: Map<String, CommandOop>)
-
Command OOP versus Functional:
interface CommandOop {
fun action(board: Board?, args: List<String>) : Board?
fun show(board: Board)
val syntax : String
}
...
object CmdQuitOop : CommandOop {
override fun action(board: Board?, args: List<String>) = null
override fun show(board: Board) {}
override val syntax: String get() = "quit"
}
|
class CommandFp (
val action: (Board?, List<String>) -> Board?,
val show: (Board?) -> Unit,
val syntax : String
)
...
val cmdQuitFp = CommandFp(
action = { _, _ -> null },
show = { },
syntax = "quit"
)
|
- Generic classes with type parameters, e.g.
class A<T> { ... }
- Generic functions with type parameters, e.g.
fun <T> foo() { ... }
- Generic classes or functions are used with type arguments:
- If the type arguments can be inferred, for example, from the actual parameters, you can omit the type arguments
- E.g.
Board
may be inferred in readCommandsOop<Board>(mapOf("QUIT" to QuitCommandOop, ..))
- Refactor
tictactoe-ui
with an auxiliary ui-generic
module.
- Kotlin Lambdas
-
return
with label e.g. return@filter
- Function Reference
- Standard input and output, i.e.
System.in
and System.out
- Redirect standard IO, i.e.
System.setIn()
and System.setOut()
-
java.io
and InputStream
and OutputStream
hierarchy.
- Filters e.g.
PrintStream
- IO in memory
ByteArrayInputStream
and ByteArrayOutputStream
- Implementing an utility function
fun redirectInOut(stmts: List<String>, block: () -> Unit) : List<String>
interface Storage<K, T> {
fun new(id: K): T
fun load(id: K): T?
fun save(id: K, obj: T)
fun delete(id: K)
}
-
Serialization - process of translating object state into a stream that can be stored or transmitted, and reconstructed.
serialize(object) -> stream
deserialize(stream) -> object
- Resulting stream can be in binary or text format (e.g. XML, YAML, JSON, or other).
- Given
obj: Any
then deserialize(serialize(obj)) == obj
- Define interface for String serialization:
interface StringSerializer<T> {
fun write(obj: T): String
fun parse(input: String): T
}
- Unit tests for
FileStorage
with a simple DummyEntity
- Serialization of a complex graph of objects.
- E.g.
Board
--->
Move
--->
Position
- Implement auxiliary functions
serialize()
and deserializeTo...()
on each entity: Board
, Move
.
- Unit test for
FileStorage<String, Board>
with a StringSerializer<Board>
- Serialization of a complex graph of objects.
- E.g.
Board
--->
Move
--->
Position
- Implement auxiliary functions
serialize()
and deserializeTo...()
on each entity: Board
, Move
.
- Unit test for
FileStorage<String, Board>
with a StringSerializer<Board>
- Serialization of
Board
with different kind of boards: BoardRun
, BoardDraw
and BoardWin
.
-
KClass
- Kotlin class information of a class.
- Deserialization based on information of
KClass
of each object.
when(kind) {
BoardRun::class.simpleName -> BoardRun(moves, moves.last().player)
BoardDraw::class.simpleName -> BoardDraw(moves)
BoardWin::class.simpleName -> BoardWin(moves, moves.last().player)
}
-
MyClass::class
- getting the runtime reference to a Kotlin class.
- Remove argument Player (
X | O
) of command play, e.g. play X 1 1
.
- Instead of
play X 1 1
now should be play 1 1
.
- Each
tictactoe-ui
process should keep its Player
and the Board
.
- New entity
Game
.
data class Game(val name: String, val board: Board, val player: Player = CROSS)
- Reimplement commands for
Game
, i.e. CommandOop<Game>
- New commands depend of
Storage<String, Board>
- Deploy and run tictactoe UI:
gradlew lesson17-tictactoe-ui-storage:jar
java -jar lesson17-tictactoe-ui-storage\build\libs\lesson17-tictactoe-ui-storage.jar
- MongoDB data model: db
-->*
collection -->*
document
-
collection like a folder or table and document like a file or row.
- cloud.mongodb.com: organization
-->*
project -->*
cluster -->*
db
- Implement
class MongoStorage<K : String, T>(...) : Storage<K, T>
tictactoe-ui-storage-mongo
Compose Mental Model:
-
Declarative UI model
=>
regenerate the entire screen from scratch, then apply only the necessary changes.
-
Compose is a declarative UI framework - build UI by defining a set of composable functions.
-
composable function = take in data and emit UI elements. E.g.:
@Composable fun Greeting(name: String) { Text("Hello $name") }
-
composable function is idempotent and free of side-effects.
-
indempotent - behaves the same way when called multiple times with the same argument.
-
free of side-effects, such as modifying properties or global variables.
- When the user interacts with the UI, the UI raises Events e.g.
onClick
.
-
Events
=>
notify the app logic =>
change the app's State
-
State changes
=>
composable functions are called with new data =>
UI elements are redrawn
-
Recomposition:
- Composable functions can execute in any order
- Composable functions can run in parallel
- Recomposition skips as much as possible
-
composable function's arguments are representations of the UI State.
- Managing State
- gradle Run scripts
- InteliJ Settings - Build and run using:
Gradle
-
build.graddle.kts
:
plugins {
id('org.jetbrains.kotlin.jvm') version "1.7.20"
id("org.jetbrains.compose") version "1.2.0"
}
dependencies {
implementation(compose.desktop.currentOs)
...
}
compose.desktop {
application {
mainClass = "pt.isel.AppKt"
}
}
- TicTacToe GUI with Compose (single device without storage)
@Composable fun Cell(lin: Int, col: Int, player: Player?, action: (Position) -> Unit )
@Composable fun BoardView(board: Board, action: (Position) -> Unit)
- Blocking IO, e.g.:
- File streams:
val str = file.readText()
- MongoDB driver:
val doc = collection.findOneById(id)
- HTTP:
val body = URL(...).readText()
- Blocking IO
=>
the Result ==
Returned value, i.e. str
, doc
, body
- Blocking IO
=>
the caller's thread waits (i.e. blocking) for operation completion.
- UI Thread is blocking whenever it performs blocking IO.
- UI Thread is blocking
=>
GUI will freeze (e.g. circular tap shadow in the center of the board).
- "For decades, as developers we are confronted with a problem to solve - how to prevent our applications from blocking."
- Synchronous Calls
=>
the Result ==
Returned value (Blocking)
- Concurrent Calls
=>
many approaches:
- Multithreading:
thread { r1 = f1() ... }; r2 = f2() ...
(!!!! Blocking !!!)
- Callbacks:
f1 { err, r1 -> ... }; f2 { err, r2 -> ... }
-
Promises:
f1().then { r1 -> ... }; f2().then { r2 -> ... };
-
Async function:
async function foo { const r1 = await f1(); const r2 = await f2(); }
- Suspend function:
suspend fun foo() { val r1 = f1(); val r2 = f2(); }
-
Notice 1. despite being concurrent, it is blocking each calling thread.
-
Notice 4. and 5. are running
f1()
and f2()
NOT concurrently (although, non-blocking).
-
requestChuckNorrisParallel()
- New thread to perform blocking IO.
- Avoid it !!!! Threads are for CPU bound work!
-
requestChuckNorrisNio()
- With non-blocking IO via native JDK HttpClient
.
- How to choose third party library for non-blocking IO?
- Popularity
- Idiomatic API according to the programming environment, e.g. kotlin idiomatic suspend functions.
- Reliability => ensure it really uses native non-blocking IO.
- !!! NOTICE !!! Many libraries with Asynchronous API are not using non-blocking IO techniques.
- Using
Ktor
library to implement suspend function requestChuckNorris()
-
Coroutine - instance of suspendable computation.
- A coroutine is NOT bound to any particular thread.
-
Structured Concurrency:
- new coroutines can be only launched in a specific CoroutineScope, which delimits the lifetime of the coroutine.
- ensures that coroutines are not lost and do not leak.
- An outer scope cannot complete until all its children coroutines complete.
- Special functions:
-
launch {...}
- coroutine builder that starts a separate coroutine and returns a Job
<=>
Promise<Void>
-
async {...}
- coroutine builder that starts a separate coroutine and returns a Deferred<T>
<=>
Promise<T>
-
delay()
- special suspending function
-
runBlocking {...}
- coroutine builder that bridges the non-coroutine world with coroutines.
-
coroutineScope {...}
- used inside any suspending function to perform multiple concurrent operations.
- New interface
StorageAsync
for non-blocking IO
-
StorageAsync
<|----
FileStorageAsync
---->
AsynchronousFileChannel
- New
Game
extensions dealing with StorageAsync
- New
GameState
commands launching coroutines to interact with StorageAsync
-
InputDialog
for the name of the new game.
TDS-2122_T1-parte2.pdf
fun main() = application {
MaterialTheme {
val state = WindowState(width= 250.dp, height= Dp.Unspecified)
Window(onCloseRequest= ::exitApplication, state= state, title= "Occupancy") {
MainContent()
}
}
}
- Primary and secondary constructors.
-
init
- Initialization code in initializer blocks.
- Default values in constructor parameters.
- Defining properties in the primary constructor.
- Getter in properties readOnly(
val
)
- Setter on read/write properties (
var
)
- Getters and Setters on global properties.
- Late-init read/write properties (
lateinit
)
- Delegated properties (
by
)
TDS-2122_T2.pdf
TDS-2122_T2.pdf