This article will take about 8 minutes to read.
Functional programming paradigms can reduce the complexity and risk of new code. It contrasts with Object oriented programming, but before we can look at functional programming, we need to go over some definitions.
Object-Oriented Programming (OOP) refers to a type of computer programming in which programmers define not only the data type of a data structure, but also the types of operations that can be applied to the data structure.
This bundles together both the data and the cuntions whcare are acting upon that data. It cares about what an Object is, what state it is in, and what it can do.
Functional programming (FP) is a programming paradigm that treats computation as the evaluation of mathematical functions and avoids changing state and mutable data. It is the process of building software by
Functional programming is declarative rather than imperative, and application state flows through pure functions.
Object oriented languages are good when
Functional languages are good when
The type of code that you are adding will affect which paradigm you adhere to most closely. When adding new objects:
When adding new functionality:
It’s worth noting that there are fewer antipatterns reported for the functional paradigm. This is in large part due to its focus on immutability and statelessness.
One of the core concepts of FP is immutability
Kotlin defines mutable and immutable types for many collections data structures, such as
The default in these cases is to use the immutable type.
When doing java interop, we treat ArrayList
s as MutableList
s.
Note that operations which are done on a mutably typed object which has been cast to an immutable object will be able to modify it.
val numbers: MutableList<Int> = mutableListOf(1, 2, 3)
val readOnlyView: List<Int> = numbers
println(numbers) // [1, 2, 3]
numbers.add(4)
println(readOnlyView) // [1, 2, 3, 4]
readOnlyView.clear() // ERROR! We can't modify a read-only list.
Domain Driven Design allows for the representation of a Sealed classes are used for representing restricted class hierarchies, when a value can have one of the types from a limited set, but cannot have any other type. They are abstract and can only have private constructors.
Their set of values is restricted like an enum, but
sealed class Expr
data class Const(val number: Double): Expr()
data class Sum(val e1: Expr, val e2: Expr): Expr()
object NotANumber: Expr
fun eval(expr: Expr): Double = when(expr) {
is Const -> expr.number
is Sum -> eval(expr.e1) + eval(expr.e2)
NotANumber -> Double.NaN // "is" is not required because we are checking if the instances are the same, instead of if their types match.
// the else clause is not required because we have covered all of the cases.
}
Delegated properties:
Using delegates, which are created using the by
keyword, we can lazily insert the value that we need, only when it is accessed.
This reduces the amount of time that it takes to create new objects.
Dependency injection can be achieved via these delegated properties and inlining functions. One example of this in practice is the Koint DI framework.
class ConfigDelegate<R, C>(
private val retainedState: WithConfig<C>?,
private val defaultConfig: C
): ReadWriteProperty<R, C> {
private var _config: C? = null
override fun getValue(thisRef: R, property: KProperty<*>): C = _config
?.also { retainedState?.config = it }
?: retainedState.config
?: defaultConfig
override fun setValue(thisRef: R, property: Kproperty<*>, value: C){
_config = value
}
interface WithConfig<C>{
var config: C
}
}
class WeatherActivity(): AppCompatActivity() {
val presenter by inject<Weatherpresenter>
}
class WeatherModule: AndroidModule() {
override fun context() = applicationContext {
context(name = "WeatherActivity") {
provide { WeatherPresenter(get()) }
}
provide { WeatherRepository(get()) }
provide { LocalDataSource(AndroidReader(applicationContext)) } bind WeatherDataSource::class
}
}
val lazyValue : String by lazy {
println("Computed")
"Hello"
}
fun main(args:Array<String>){
println(lazyValue)
println(lazyValue) // the second call will return the previously computed value
}
Every time we use higher order functions, we are creating a new Function
object. This means that the functional code will have higher memory utilization, and slower compute speeds.
Marking a function as inline will allow us to reduce overhead of these functional techniques to nearly zero. However, we have to remember to use them and we need to understand when inlining is appropriate.
inline fun <T> lock(lock: Lock, body: () -> T): T {
lock.lock()
try {
body()
}
finally {
lock.unlock()
}
}
would become the following after compilation:
lock(l) { foo() }
// Contents of the "body" lambda
l.lock()
try {
foo()
} finally {
l.unlock()
}
Reification is when we make an inline function work with “Real” types
treeNode.findParentOftype(MyTreeNode::class.java)
//...
treeNode.findParentOftype<MyTreeNode>()
Reification is not compatible with Java, because we would be referring to a generic type which has already been erased.
Since kotlin still allows for OOP, any method call can still be potentially side effecting.
There are no const functions
yet.
Immutable types can be changed if there is still a mutable instance of the variable.
The kotlin standard library does not have a high-performance immutable collections library yet (as scala does)