Wikimapia

What if you tried Kotlin?

An introduction to Kotlin

with an eye to clean code

Damiano Salvi

@PiDayDev

https://github.com/PiDayDev/

unsplash-logoMarvin Esteve

Disclaimer

Code is a fact

Opinions are my own

P.S.: brace yourselves - Emojis and GIFs are coming

😜

... I warned you

What are we going to see?

Hi Nando!

The kata

Checkout system

Simple price:

1 apple = 50 cents
    🍎           πŸ’΅

Special offers:

3 apples = 120 cents
🍎🍎🍎        πŸ’ΈπŸ’Έ

 
  • Products in any order
    🍐🍎🍐🍍🍐🍌🍐  πŸ‘†  4🍐 = πŸ€‘
  • Receive offers with each transaction
    5🍍 = πŸ’°πŸ’°

Starting point

  • JUnit tests
  • Java implementation:
    • Tests πŸ‘
    • Quality πŸ‘Ž

The interface

package it.intre.sal.kotlin101;

import java.util.*;
import java.util.Map.Entry;

public interface Checkout {
  int pay(
    List<String> items,
    Map<String, Entry<Integer, Integer>> offers
  );
}

on light background: Java

A few tests

@Test
void aBananaCosts60() throws Exception {
  assertEquals(60, checkout.pay(
    forFruits("banana"), withOffers(1, "banana", 60)
  ));
}

@Test
void fruits() {
  Map<String, Entry<Integer, Integer>> ll = withOffers(3, "apple", 130);
  ll.put("pear", new SimpleEntry<>(2, 45));
  int expectedPrice = 130 /* 3 apples */
                    +  45 /* 2 pears */
                    +  60 /* 1 banana */
                    + 220 /* 1 pineapple */;
  assertEquals(expectedPrice, checkout.pay(
    forFruits("apple", "pear", "apple", "pear", "lychee", "apple", "banana", "pineapple"),
    ll));
}

simplest offer (discount)

two offers, many items

The implementation

public class UglyCheckout implements Checkout {

    @Override
    public int pay(List<String> items, Map<String, Entry<Integer, Integer>> offers) {
        int res = 0;
        int a = 0;
        int p = 0;
        int ananas = 0;
        int b = 0;

        Map<String, Integer> map = new HashMap<>();
        map.put("apple", 50);
        map.put("pear", 30);
        map.put("pineapple", 220);
        map.put("banana", 60);

        for (String item : items) {
            switch (item) {
                case "apple":
                    a++;
                    break;
                case "pear":
                    p++;
                    break;
                case "pineapple":
                    ananas++;
                    break;
                case "banana":
                    b++;
                    break;
            }
        }

        //Here I have to cycle through every offer to see if it applies
        for (Entry entry : offers.entrySet()) {
            switch (entry.getKey().toString()) {
                case "apple":
                    int a1 = (int) ((Entry) entry.getValue()).getKey();
                    int c1 = a / a1;
                    if (a >= a1) {
                        res += c1 * (int) ((Entry) entry.getValue()).getValue();
                    }
                    a -= a1 * c1;
                    break;
                //jb 2008-09-12: don't sell lychee anymore, but maybe in the future...
//                case "lychee":
//                    int a2 = (int) ((Entry) entry.getValue()).getKey();
//                    if (p >= a2) { res += (int) ((Entry) entry.getValue()).getValue(); }
//                    p -= a2;
//                    break;
                case "pear":
                    int a2 = (int) ((Entry) entry.getValue()).getKey();
                    int c2 = p / a2;
                    if (p >= a2) {
                        res += c2 * (int) ((Entry) entry.getValue()).getValue();
                    }
                    p -= a2 * c2;
                    break;
                case "pineapple":
                    int a3 = (int) ((Entry) entry.getValue()).getKey();
                    int c3 = ananas / a3;
                    if (ananas >= a3) {
                        res += c3 * (int) ((Entry) entry.getValue()).getValue();
                    }
                    ananas -= a3 * c3;
                    break;
                case "banana":
                    int a4 = (int) ((Entry) entry.getValue()).getKey();
                    int c4 = b / a4;
                    if (b >= a4) {
                        res += c4 * (int) ((Entry) entry.getValue()).getValue();
                    }
                    b -= a4 * c4;
                    break;
            }
        }

        for (Entry entry : map.entrySet()) {
            switch (entry.getKey().toString()) {
                case "apple":
                    res += a * (int) entry.getValue();
                    break;
                case "pear":
                    res += p * (int) entry.getValue();
                    break;
                case "pineapple":
                    res += ananas * (int) entry.getValue();
                    break;
                case "banana":
                    res += b * (int) entry.getValue();
                    break;
            }
        }

        return res;
    }
}

        int res = 0;
        int a = 0;
        int p = 0;
        int ananas = 0;
        int b = 0;

item counters


        Map<String, Integer> map = new HashMap<>();
        map.put("apple", 50);
        map.put("pear", 30);
        map.put("pineapple", 220);
        map.put("banana", 60);

price map


        for (String item : items) {
            switch (item) {
                case "apple":
                    a++;
                    break;
                case "pear":
                    p++;
                    break;
                case "pineapple":
                    ananas++;
                    break;
                case "banana":
                    b++;
                    break;
            }
        }

count items


    //Here I have to cycle through every offer to see if it applies
    for (Entry entry : offers.entrySet()) {
        switch (entry.getKey().toString()) {
            case "apple":
                int a1 = (int) ((Entry) entry.getValue()).getKey();
                int c1 = a / a1;
                if (a >= a1) {
                    res += c1 * (int) ((Entry) entry.getValue()).getValue();
                }
                a -= a1 * c1;
                break;

           /* ... */
    }

apply offers, reduce quantities


        for (Entry entry : map.entrySet()) {
            switch (entry.getKey().toString()) {
                case "apple":
                    res += a * (int) entry.getValue();
                    break;

                /* ... */
            }
        }

residual quantities

 

How I solved the kata

  • Refactor to a cleaner version! πŸ”„
  • Could I have done it in Java?
    • Absolutely βœ…
    • Ample room for improvement πŸ˜…
  • But JVM doesn't mean just Java πŸ’‘
  • Why not try something else?
    • To learn πŸŽ“
    • To (maybe) overcome some limits of the same old language πŸš€

clean up with style!

So what?

Kotlin is...

  • created by JetBrains
  • modern
  • concise
  • expressive
  • intuitive
  • multi-paradigm
  • 100% compatible with Java & JVM
  • supported for Android development
  • influenced by Effective Java (J.Bloch)
  • ...
fun times() =
  with (colleagues) {
    studying {
      Kotlin in guild
    }
  }

val Kotlin = "K"

val guild = "OK!"

val colleagues = "IntrΓ©"

fun studying(
  what: () -> Boolean
) = what()
unsplash-logoRick Mason

Why I like it

fluent and concise

string templates delegates immutable by default
infix functions Android inline functions { it }
lambda higher-order functions var & val extension functions
operator overload DSL public by default coroutines
non-nullable types data class one-line classes
Java compatibility collection functions properties
unsplash-logoShane Aldendorff

Disclaimer (again)

We are NOT going to...

  • remove every code smell
  • get a flawless implementation
  • see a complete overview of Kotlin

...but we WILL

  • reduce code smells
  • make the code clearer and more expressive
  • discover some Kotlin features
  • spur your curiosity (at least, I hope so πŸ˜‡πŸ€ž)

A Kotlin class



class ImprovedCheckout

one-line class

; is optional

❌

on dark background: Kotlin

package it.intre.sal.kotlin101
 
 

Implement the Java interface

class ImprovedCheckout : Checkout {

    override fun pay(
        items: List<String>,
        offers: Map<String, Map.Entry<Int, Int>>
    ): Int {
        TODO("not implemented")
    }

}
implements πŸ‘† ":"
 
fun name(..): Type
 
 

mandatory override

 

nice TODO function

 

extensions from standard library

 
 
package kotlin.collections

public interface Collection<out E> : Iterable<E> { /* ... */ }
public interface List<out E> : Collection<E> { /* ... */ }
public interface Set<out E> : Collection<E> { /* ... */ }
public interface Map<K, out V> { /* ... */ }

100% interoperability also on Java side

class CheckoutTest {
    // ...

    static class Checkouts implements ArgumentsProvider {
        // ....
        return Stream.of(new UglyCheckout(), new ImprovedCheckout())
                    .map(Arguments::of);
        // ...
    }
}

new Java object

new Kotlin object

fails as expected πŸ’€

 
 
unsplash-logorawpixel

Copy Java & paste Kotlin

override fun pay(items: List<String>, offers: Map<String, Map.Entry<Int, Int>>): Int {
  var res = 0
  var a = 0
  var p = 0
  var ananas = 0
  var b = 0
  // TODO prices
  for (item in items) {
    when (item) {
      "apple" -> a++
      "pear" -> p++
      "pineapple" -> ananas++
      "banana" -> b++
    }
  }
  // TODO offers
  for ((key, value) in map) {
    when (key) {
      "apple" -> res += a * value
      "pear" -> res += p * value
      "pineapple" -> res += ananas * value
      "banana" -> res += b * value
    }
  }
  return res
}
public int pay(List<String> items, Map<String, Entry<Integer, Integer>> offers) {
    int res = 0;
    int a = 0;
    int p = 0;
    int ananas = 0;
    int b = 0;
    // ...prices...
    for (String item : items) {
        switch (item) {
            case "apple":
                a++;
                break;
            case "pear":
                p++;
                break;
            case "pineapple":
                ananas++;
                break;
            case "banana":
                b++;
                break;
        }
    }
    // ...offers...
    for (Entry entry : map.entrySet()) {
        switch (entry.getKey().toString()) {
            case "apple":
                res += a * (int) entry.getValue();
                break;
            case "pear":
                res += p * (int) entry.getValue();
                break;
            case "pineapple":
                res += ananas * (int) entry.getValue();
                break;
            case "banana":
                res += b * (int) entry.getValue();
                break;
        }
    }

    return res;
}

IntelliJ auto-converts 😍

type inference

when(..) expression

destructuring assignment

 
 
 
 
 
 
 
 
 
 

//TODO prices

val        var

immutable      mutable

const val APPLE = "apple"
const val PEAR = "pear"
const val PINEAPPLE = "pineapple"
const val BANANA = "banana"
const val

compile-time constants

idiomatic map creation

to: infix function

val prices = mapOf(
    APPLE to 50,
    PEAR to 30,
    PINEAPPLE to 220,
    BANANA to 60
)

type inference πŸ‘† can omit : String

const val APPLE: String = "apple"
const val PEAR: String = "pear"
const val PINEAPPLE: String = "pineapple"
const val BANANA: String = "banana"

All together

 
const val APPLE = "apple"
const val PEAR = "pear"
const val PINEAPPLE = "pineapple"
const val BANANA = "banana"

val prices = mapOf(
  APPLE to 50,
  PEAR to 30,
  PINEAPPLE to 220,
  BANANA to 60
)

only one failure

class ImprovedCheckout : Checkout {
  override fun pay(items: List<String>, offers: Map<String, Map.Entry<Int, Int>>): Int {
    var res = 0
    var a = 0
    var p = 0
    var ananas = 0
    var b = 0

    for (item in items) {
      when (item) {
        APPLE -> a++
        PEAR -> p++
        PINEAPPLE -> ananas++
        BANANA -> b++
      }
    }
    // TODO offers
    for ((key, value) in prices) {
      when (key) {
        APPLE -> res += a * value
        PEAR -> res += p * value
        PINEAPPLE -> res += ananas * value
        BANANA -> res += b * value
      }
    }
    return res
  }
}

//TODO offers

//Here I have to cycle through every offer to see if it applies
for ((key, value) in offers) {
  when (key) {
    "apple" -> {
      val a1 = (value as Entry<*, *>).key as Int
      val c1 = a / a1
      if (a >= a1) {
        res += c1 * (value as Entry<*, *>).value as Int
      }
      a -= a1 * c1
    }
//jb 2008-09-12: don't sell lychee anymore, but maybe in the future...
//  case "lychee":
//  int a2 = (int) ((Entry) entry.getValue()).getKey();
//  if (p >= a2) {
//     res += (int) ((Entry) entry.getValue()).getValue(); }
//    p -= a2; //    break;
    "pear" -> {
      val a2 = (value as Entry<*, *>).key as Int
      val c2 = p / a2
      if (p >= a2) {
        res += c2 * (value as Entry<*, *>).value as Int
      }
      p -= a2 * c2
    }
    "pineapple" -> {
      val a3 = (value as Entry<*, *>).key as Int
      val c3 = ananas / a3
      if (ananas >= a3) {
        res += c3 * (value as Entry<*, *>).value as Int
      }
      ananas -= a3 * c3
    }
    "banana" -> {
      val a4 = (value as Entry<*, *>).key as Int
      val c4 = b / a4
      if (b >= a4) {
        res += c4 * (value as Entry<*, *>).value as Int
      }
      b -= a4 * c4
    }
  }
}

πŸ‘Ž ugly casts

πŸ‘Ž smelly dead comments

πŸ‘Ž unclear names

 
 
 
 
 
 
 

πŸ‘Ž duplication

 
 
 

😎 but it works!

Refactoring!

var res = 0
var a = 0
var p = 0
var ananas = 0
var b = 0

for (item in items) {
    when (item) {
        APPLE -> a++
        PEAR -> p++
        PINEAPPLE -> ananas++
        BANANA -> b++
    }
}

//Here I have to cycle through every offer to see if it applies
for ((key, value) in offers) {
    when (key) {
        "apple" -> {
            val a1 = (value as Entry<*, *>).key as Int
            val c1 = a / a1
            if (a >= a1) {
                res += c1 * (value as Entry<*, *>).value as Int
            }
            a -= a1 * c1
        }
//jb 2008-09-12: don't sell lychee anymore, but maybe in the future...
//  case "lychee":
//  int a2 = (int) ((Entry) entry.getValue()).getKey();
//  if (p >= a2) {
//     res += (int) ((Entry) entry.getValue()).getValue(); }
//    p -= a2; //    break;
var res = 0
var apple = 0
var pear = 0
var pineapple = 0
var banana = 0

for (item in items) {
    when (item) {
        APPLE -> apple++
        PEAR -> pear++
        PINEAPPLE -> pineapple++
        BANANA -> banana++
    }
}


for ((item, offer) in offers) {
    when (item) {
        APPLE -> {
            val a1 = offer.key
            val q = apple / a1
            if (apple >= a1) {
                res += q * offer.value
            }
            apple -= a1 * q
        }
        PEAR -> {
            val a2 = offer.key
            val q = pear / a2
            if (pear >= a2) {
                res += q * offer.value
            }
            pear -= a2 * q
 

meaningful names

constants

R.I.P. πŸ‘»

no casts

 
 
 
 
 
 
 
 
 
 
 
 
 

Refactoring!

    "pineapple" -> {
      val a3 = (value as Entry<*, *>).key as Int
      val c3 = ananas / a3
      if (ananas >= a3) {
        res += c3 * (value as Entry<*, *>).value as Int
      }
      ananas -= a3 * c3
    }
    "banana" -> {
      val a4 = (value as Entry<*, *>).key as Int
      val c4 = b / a4
      if (b >= a4) {
        res += c4 * (value as Entry<*, *>).value as Int
      }
      b -= a4 * c4
    }
  }
}

for ((key, value) in prices) {
    when (key) {
        APPLE -> res += a * value
        PEAR -> res += p * value
        PINEAPPLE -> res += ananas * value
        BANANA -> res += b * value
    }
}

return res
        PINEAPPLE -> {
            val a3 = offer.key
            val q = pineapple / a3
            if (pineapple >= a3) {
                res += q * offer.value
            }
            pineapple -= a3 * q
        }
        BANANA -> {
            val a4 = offer.key
            val q = banana / a4
            if (banana >= a4) {
                res += q * offer.value
            }
            banana -= a4 * q
        }
    }
}

for ((item, price) in prices) {
    when (item) {
        APPLE -> res += apple * price
        PEAR -> res += pear * price
        PINEAPPLE -> res += pineapple * price
        BANANA -> res += banana * price
    }
}

return res
 

names again

 
 

D.R.Y.

var res = 0
var apple = 0
var pear = 0
var pineapple = 0
var banana = 0

for (item in items) {
    when (item) {
        APPLE -> apple++
        PEAR -> pear++
        PINEAPPLE -> pineapple++
        BANANA -> banana++
    }
}

for ((item, offer) in offers) {
    when (item) {
        APPLE -> {
            val a1 = offer.key
            val q = apple / a1
            if (apple >= a1) {
                res += q * offer.value
            }
            apple -= a1 * q
        }
        //  ... REPEAT THREE MORE TIMES 😣
}

for ((item, price) in prices) {
    when (item) {
        APPLE -> res += apple * price
        PEAR -> res += pear * price
        PINEAPPLE -> res += pineapple * price
        BANANA -> res += banana * price
    }
}

return res
var res = 0

val quantities = mutableMapOf<String, Int>()

for (item in items) {
    quantities[item] = 1 + (quantities[item] ?: 0)
}


for ((item, offer) in offers) {
    val (offerQuantity, offerPrice) = offer
    val quantity = quantities[item] ?: 0
    if (item in prices.keys) {
        val repeat = quantity / offerQuantity
        res += repeat * offerPrice
        quantities[item] = quantity - repeat * offerQuantity
    }
}




for ((item, price) in prices) {
    val quantity = quantities[item] ?: 0
    res += quantity * price
}




return res
 

one map πŸ’

(to count them all)

one computation

?: Elvis operator

destr.assign. again

 
 
 
 
 
 
 
 

Collection methods

val quantities = mutableMapOf<String, Int>()
for (item in items) {
    quantities[item] = 1 + (quantities[item] ?: 0)
}


for ((item, offer) in offers) {
    // ...
}


for ((item, price) in prices) {
    val quantity = quantities[item] ?: 0
    res += quantity * price
}
val quantities = items
        .groupingBy( { item -> item } )
        .eachCount()
        .toMutableMap()


offers.forEach { (item, offer) ->
    // ...
}


res += quantities.entries.sumBy {
    (item, quantity) -> quantity * (prices[item]?:0)
}
 

no-boilerplate collection functions

 
 
 

lambda expressions

 
 
 

mutable? on-demand

 

inline fun: embed lambda ⏩

inline fun <T> Iterable<T>.forEach(action: (T) -> Unit)

Lambda expressions

 items.groupingBy( { item -> item } )



 items.groupingBy() { item -> item }



 items.groupingBy { item -> item }



 items.groupingBy { it }

lambda expressions as an argument

lambda can go outside "( )" if last argument

and you can omit "( )" if only argument

lambda with a single parameter can use implicit it

 
 
 
 
CTRL + ALT + SHIFT + K
public interface Checkout {
    int pay(List<String> items, Map<String, Map.Entry<Integer, Integer>> offers);
}
class CheckoutTest {
    // ...
    void aBananaCosts60(Checkout checkout) {
        assertEquals(60, checkout.pay(
            forFruits("banana"), withOffers(1, "banana", 60)
        ));
    }
}
interface Checkout {
    fun pay(items: List<String>, offers: Map<String, Map.Entry<Int, Int>>): Int
}
class CheckoutTest {
    // ...
    fun aBananaCosts60(checkout: Checkout) {
        assertEquals(60, checkout.pay(
            forFruits("banana"), withOffers(1, "banana", 60)
        ))
    }
}

Disclaimer: may need πŸ‘©β€πŸ”§πŸ‘¨β€πŸ”§

Benefits

fun pay(items: List<String>, offers: Map<String, Map.Entry<Int, Int>>): Int

fun pay(items: List<String>, offers: Map<String, Pair<Int, Int>>): Int


fun withOffers(quantity: Int, fruit: String, offerPrice: Int): MutableMap</*..*/> {
    val offers = HashMap<String, Entry<Int, Int>>()
    offers[fruit] = SimpleEntry(quantity, offerPrice)
    return offers
}

fun withOffers(quantity: Int, fruit: String, offerPrice: Int) =
    mutableMapOf(fruit to (quantity to offerPrice))


fun forFruits(vararg fruits: String): List<String> {
    return Arrays.asList(*fruits)
}

fun forFruits(vararg fruits: String) = fruits.asList()

Pair idioms: a to (b to c)

collection methods

 
 
 
 
 
 

builtin types

We can do better

override fun pay(items: List<String>, offers: Map<String, Pair<Int, Int>>): Int {
    val quantities = items
            .groupingBy { it }
            .eachCount()
            .toMutableMap()

    var offerTotal = 0
    offers.forEach { (item, offer) ->
        val (offerQuantity, offerPrice) = offer
        val quantity = quantities[item]
        if (quantity != null && item in prices.keys) {
            val repeat = quantity / offerQuantity
            offerTotal += repeat * offerPrice
            quantities[item] = quantity - repeat * offerQuantity
        }
    }

    return offerTotal +
           quantities.entries.sumBy { (item, quantity) ->
                quantity * (prices[item] ?: 0)
           }
}

mutable map πŸ˜•

mutable var πŸ™„

if 🀨

primitive obsession πŸ€ͺ

long method πŸ˜₯

 
 
 
 
 
 
 
 
 
 

The first rule of functions is that they should be small.

     The second rule of functions is that they should be smaller than that.

Uncle Bob

Domain model

data class Offer(val quantity: Int, val price: Int)
data class

smaller methods

new instance (no new)

named arguments

override fun pay(items: List<String>, offers: Map<String, Pair<Int, Int>>): Int {
    val quantities = items.groupingBy { it }.eachCount().toMutableMap()








    var offerTotal = 0
    offers.forEach { (item, offer) ->
        val (offerQuantity, offerPrice) = offer
        // ...
    }
    // ...
    val offersMap = offers.mapValues {
         (_, v) -> Offer(quantity = v.first, price = v.second)
    }
    return pay(quantities, offersMap)
}

private fun pay(quantities: MutableMap<String, Int>, offers: Map<String, Offer>): Int {

built-in support for destructuring assignment, ...

 
 
 
 
 
 

Let's move business logic into the model

expression body

 

operator overload

 
Offer * 3 ⇄ Offer.times(3)
data class Offer(val quantity: Int, val price: Int) {

    operator fun times(repeat: Int) = Offer(repeat * quantity, repeat * price)

}
data class Offer(val quantity: Int, val price: Int) {

    operator fun times(repeat: Int) = Offer(repeat * quantity, repeat * price)

    infix fun buying(quantity: Int) : Offer {
        val repeat = quantity / this.quantity
        return this * repeat
    }

}
 

infix function

i call u ⇄ i.call(u)

and that's how DSL are born πŸ‘Ά

 
  val (offerQuantity, offerPrice) = offer
  val repeat = quantity / offerQuantity
  offerTotal += repeat * offerPrice
  quantities[item] = quantity - repeat * offerQuantity
 
  val appliedOffer = offer buying quantity
  offerTotal += appliedOffer.price
  quantities[item] = quantity - appliedOffer.quantity

Model for "no offers"

data class Offer(val quantity: Int, val price: Int) {
    operator fun times(repeat: Int) = Offer(repeat * quantity, repeat * price)
    infix fun buying(quantity: Int) : Offer {
        val repeat = quantity / this.quantity
        return this * repeat
    }
}
sealed class SpecialPrice

sealed restricts inheritance

all subclasses in same file

object makes a singleton

data class Offer(val quantity: Int, val price: Int): SpecialPrice() {
    operator fun times(repeat: Int) = Offer(repeat * quantity, repeat * price)
    infix fun buying(quantity: Int) : Offer {
        val repeat = quantity / this.quantity
        return this * repeat
    }
}
object NoOffer : SpecialPrice()
 
 
 
sealed class SpecialPrice {
  companion object {
    fun from(quantityToPrice: Pair<Int, Int>?) =
      when (quantityToPrice) {
        null -> NoOffer
        else -> Offer(
          quantity = quantityToPrice.first,
          price = quantityToPrice.second
        )
      }
  }
}

companion object β‰ˆ Java static

when & null only in factory method

 
 
 

There may be no more than one switch statement for a given type of selection.
    The cases [...] must create polymorphic objects that take the place of other such switch [...]

Uncle Bob

Polymorphism

sealed class SpecialPrice {
  companion object { /* ... */ }
  abstract fun payItem(quantity: Int, price: Int): Int
}

data class Offer(val quantity: Int, val price: Int): SpecialPrice() {
  /* ... */

  override fun payItem(quantity: Int, price: Int): Int {



  }
}

object NoOffer: SpecialPrice() {
  override fun payItem(quantity: Int, price: Int) =
}
 

Offer API

quantity * price
    val appliedOffer = this buying quantity
    return appliedOffer.price + (quantity - appliedOffer.quantity) * price
? ? ?
? ? ?
 
 

Classes in action

override fun pay(items: List<String>, offers: Map<String, Pair<Int, Int>>): Int {
  val quantities = items
    .groupingBy { it }
    .eachCount()
    .toMutableMap()

  val offersMap = offers.mapValues {
    (_, v) -> Offer(quantity = v.first, price = v.second)
  }
  return pay(quantities, offersMap)
}

private fun pay(quantities: MutableMap<String, Int>, offers: Map<String, Offer>): Int {
  var offerTotal = 0
  offers.forEach { (item, offer) ->
    val (offerQuantity, offerPrice) = offer
    val quantity = quantities[item]
    if (quantity != null && item in prices.keys) {
      val repeat = quantity / offerQuantity
      offerTotal += repeat * offerPrice
      quantities[item] = quantity - repeat * offerQuantity
    }
  }

  return offerTotal +
    quantities.entries.sumBy { (item, quantity) -> quantity * (prices[item] ?: 0) }
}
override fun pay(items: List<String>,
         offers: Map<String, Pair<Int,Int>>) =
  prices.entries.sumBy { (item, price) ->
    val quantity = items.count { it == item }
    val offer = SpecialPrice.from(offers[item])
    offer.payItem(quantity, price)
  }
 

loop once on non-null Pairs (item to price)

 

sumBy: for each entry, add lambda result

 

count each item separately (easier)

 

from Pair? get a non-null SpecialPrice

 

lambda returns value of last expression

 

no if, null, var, Mutable*

🀩😎

small!

Recap

  public int pay(List<String> items, Map<String, Entry<Integer, Integer>> offers) {
    int res = 0;
    int a = 0;
    int p = 0;
    int ananas = 0;
    int b = 0;

    Map<String, Integer> map = new HashMap<>();
    map.put("apple", 50);
    map.put("pear", 30);
    map.put("pineapple", 220);
    map.put("banana", 60);

    for (String item : items) {
      switch (item) {
        case "apple":
          a++;
          break;
        case "pear":
          p++;
          break;
        case "pineapple":
          ananas++;
          break;
        case "banana":
          b++;
          break;
      }
    }
    //Here I have to cycle through every offer to see if it applies
    for (Entry entry : offers.entrySet()) {
      switch (entry.getKey().toString()) {
        case "apple":
          int a1 = (int) ((Entry) entry.getValue()).getKey();
          if (a >= a1) {
            res += (int) ((Entry) entry.getValue()).getValue();
          }
          a -= a1;
          break;
        //jb 2008-09-12: don't sell lychee anymore, but maybe in the future...
//                case "lychee":
//                    int a2 = (int) ((Entry) entry.getValue()).getKey();
//                    if (p >= a2) { res += (int) ((Entry) entry.getValue()).getValue(); }
//                    p -= a2;
//                    break;
        case "pear":
          int a2 = (int) ((Entry) entry.getValue()).getKey();
          if (p >= a2) {
            res += (int) ((Entry) entry.getValue()).getValue();
          }
          p -= a2;
          break;
        case "pineapple":
          int a3 = (int) ((Entry) entry.getValue()).getKey();
          if (ananas >= a3) {
            res += (int) ((Entry) entry.getValue()).getValue();
          }
          ananas -= a3;
          break;
        case "banana":
          int a4 = (int) ((Entry) entry.getValue()).getKey();
          if (b >= a4) {
            res += (int) ((Entry) entry.getValue()).getValue();
          }
          b -= a4;
          break;
      }
    }

    for (Entry entry : map.entrySet()) {
      switch (entry.getKey().toString()) {
        case "apple":
          res += a * (int) entry.getValue();
          break;
        case "pear":
          res += p * (int) entry.getValue();
          break;
        case "pineapple":
          res += ananas * (int) entry.getValue();
          break;
        case "banana":
          res += b * (int) entry.getValue();
          break;
      }
    }

    return res;
  }
}
class ImprovedCheckout : Checkout {
  override fun pay(items: List<String>, offers: Map<String, Pair<Int, Int>>) =
    prices.entries.sumBy { (item, price) ->
      val quantity = items.count { it == item }
      val offer = SpecialPrice.from(offers[item])
      offer.payItem(quantity, price)
    }
}
sealed class SpecialPrice {
  companion object {
    fun from(quantityToPrice: Pair<Int, Int>?) = when (quantityToPrice) {
        null -> NoOffer
        else -> Offer(quantity = quantityToPrice.first, price = quantityToPrice.second)
      }
  }
  abstract fun payItem(quantity: Int, price: Int): Int
}
data class Offer(val quantity: Int, val price: Int) : SpecialPrice() {
  operator fun times(repeat: Int) = Offer(repeat * quantity, repeat * price)
  private infix fun buying(quantity: Int): Offer {
    val repeat = quantity / this.quantity
    return this * repeat
  }
  override fun payItem(quantity: Int, price: Int): Int {
    val appliedOffer = this buying quantity
    return appliedOffer.price + (quantity - appliedOffer.quantity) * price
  }
}
object NoOffer : SpecialPrice() {
  override fun payItem(quantity: Int, price: Int) = quantity * price
}








   /* this space intentionally left blank */

Kotlin ecosystem

"State of Kotlin 2018" research πŸ“ˆ

Stack Overflow blog 2017: 2nd least hated 🀷

Stack Overflow survey 2018: 2nd most loved πŸ’œ

Future looks bright

...although it may not be the right solution for everyone

Kotlin Foundation

The Kotlin Foundation was created by JetBrains and Google with the mission to protect, promote and advance the development of the Kotlin programming language.

References

πŸ’» Source code (step by step)
      https://github.com/PiDayDev/kotlin-what-if-you-tried

🎞️ Slides
      https://pidaydev.github.io/kotlin-what-if-you-tried-slides

πŸ“— "Clean Code: A Handbook of Agile Software Craftsmanship"
      Robert C. Martin (a.k.a. Uncle Bob)

πŸ“™ "Effective Java"
      Joshua Bloch

πŸ“’ Kotlin Programming Language
      https://kotlinlang.org/

πŸ“Š State of Kotlin 2018, Pusher
      https://pusher.com/state-of-kotlin

Thanks

 
unsplash-logoEmily Morter

@PiDayDev

https://github.com/PiDayDev/