Skip to content

Commit

Permalink
minimal tests for lru caches
Browse files Browse the repository at this point in the history
  • Loading branch information
gciatto committed May 6, 2020
1 parent 0650aa2 commit 05656ed
Show file tree
Hide file tree
Showing 8 changed files with 170 additions and 25 deletions.
45 changes: 41 additions & 4 deletions core/src/commonMain/kotlin/it/unibo/tuprolog/utils/Cache.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,57 @@ package it.unibo.tuprolog.utils
import it.unibo.tuprolog.utils.impl.LRUCache
import it.unibo.tuprolog.utils.impl.SimpleLRUCache

/**
* Mutable, fixed-capacity cache whose eviction strategy depends on the specific implementation
* @param K is the type of the keys used for indexing items in this cache
* @param V is the type of the values stored in this cache
*/
interface Cache<K, V> {
val capacity: Int

operator fun set(key: K, value: V): Optional<out K>

operator fun get(key: K): V?
/**
* Retrieves the maximum amount of items this cache may ever store
*/
val capacity: Int

/**
* Retrieves the amount of items currently cached by this cache
*/
val size: Int

/**
* Stores a new key-value pair in this cache, possibly evicting some previously stored key-value pair
* @param key is the key used for indexing the pair
* @param value is the value corresponding to [key]
* @return the evicted key-value pair, if any
*/
operator fun set(key: K, value: V): Optional<out Pair<K, V>>

/**
* Retrieves the cached value corresponding to the provided [key]
* @param key is the key used for indexing the pair
* @return the value corresponding to [key], if any
*/
operator fun get(key: K): Optional<out V>

/**
* Converts this cache to an immutable map
*/
fun toMap(): Map<K, V>

/**
* Converts this cache to a sequenve of key-value pairs
*/
fun toSequence(): Sequence<Pair<K, V>>

companion object {
/**
* Creates a new LRU (least recently used) cache
*/
fun <K, V> lru(capacity: Int = 5): Cache<K, V> = LRUCache(capacity)

/**
* Creates a new LRU (least recently used) cache, using a simpler (less memory-consuming) implementation
*/
fun <K, V> simpleLru(capacity: Int = 5): Cache<K, V> = SimpleLRUCache(capacity)
}
}
11 changes: 9 additions & 2 deletions core/src/commonMain/kotlin/it/unibo/tuprolog/utils/Optional.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,16 @@ package it.unibo.tuprolog.utils
sealed class Optional<T> {

companion object {
fun <T> of(value: T): Optional<out T> = Some(value)
fun <T> some(value: T): Some<out T> = Some(value)

fun <T> empty(): Optional<out T> = None
fun <T> of(value: T?): Optional<out T> =
if (value == null) {
none()
} else {
some(value)
}

fun <T> none(): Optional<out T> = None
}

data class Some<T>(override val value: T) : Optional<T>() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,33 @@ internal class LRUCache<K, V>(override val capacity: Int) : Cache<K, V> {
}

private val cache = mutableMapOf<K, V>()
private val insertionOrder = (0 until capacity).map { Optional.empty<K>() }.toTypedArray()
private val size get() = cache.size
private val insertionOrder = (0 until capacity).map { Optional.none<K>() }.toTypedArray()
private var nextFreeIndex = 0

override fun set(key: K, value: V): Optional<out K> {
val evicted = insertionOrder[nextFreeIndex].also {
if (it is Optional.Some) {
cache.remove(it.value)
override val size get() = cache.size

override fun set(key: K, value: V): Optional<out Pair<K, V>> {
val evicted: Optional<out Pair<K, V>> = insertionOrder[nextFreeIndex].let { evictedKey ->
if (evictedKey is Optional.Some) {
val evictedValue = cache[evictedKey.value]!!
cache.remove(evictedKey.value)
Optional.some(evictedKey.value to evictedValue)
} else {
Optional.none()
}
}
insertionOrder[nextFreeIndex] = Optional.of(key)
insertionOrder[nextFreeIndex] = Optional.some(key)
cache[key] = value
nextFreeIndex = (nextFreeIndex + 1) % capacity
return evicted
}

override fun get(key: K): V? =
cache[key]
override fun get(key: K): Optional<out V> =
if (cache.containsKey(key)) {
Optional.of(cache[key])
} else {
Optional.none()
}

override fun toMap(): Map<K, V> =
cache.toMap()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,32 @@ internal class SimpleLRUCache<K, V>(override val capacity: Int) : Cache<K, V> {

private val cache = LinkedHashMap<K, V>()

override fun set(key: K, value: V): Optional<out K> {
override fun set(key: K, value: V): Optional<out Pair<K, V>> {
val evicted = removeLeastRecentIfNecessary()
cache[key] = value
return evicted
}

private fun removeLeastRecent(): Optional<out K> {
val key = cache.iterator().next().key
cache.remove(key)
return Optional.of(key)
private fun removeLeastRecent(): Optional<out Pair<K, V>> {
val entry = cache.iterator().next()
cache.remove(entry.key)
return Optional.some(entry.toPair())
}

private fun removeLeastRecentIfNecessary(): Optional<out K> {
return if (cache.size > capacity) {
private fun removeLeastRecentIfNecessary(): Optional<out Pair<K, V>> {
return if (cache.size >= capacity) {
removeLeastRecent()
} else {
return Optional.empty()
return Optional.none()
}
}

override fun get(key: K): V? =
cache[key]
override fun get(key: K): Optional<out V> =
if (cache.containsKey(key)) {
Optional.of(cache[key])
} else {
Optional.none()
}

override fun toMap(): Map<K, V> =
cache.toMap()
Expand Down Expand Up @@ -62,4 +66,7 @@ internal class SimpleLRUCache<K, V>(override val capacity: Int) : Cache<K, V> {
override fun toString(): String {
return "SimpleLRUCache(${toSequence().map { "${it.first} = ${it.second}" }.joinToString(", ")})"
}

override val size: Int
get() = cache.size
}
57 changes: 57 additions & 0 deletions core/src/commonTest/kotlin/it/unibo/tuprolog/utils/CacheTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package it.unibo.tuprolog.utils

import kotlin.test.assertEquals

interface CacheTest {

companion object {

fun prototype(cacheConstructor: (Int) -> Cache<String, Int>): CacheTest =
object : CacheTest {
override fun cacheFactory(capacity: Int): Cache<String, Int> {
return cacheConstructor(capacity)
}
}

private val ITEMS: List<Pair<String, Int>> = (0 until 26).asSequence()
.map { String(charArrayOf('a' + it)) to it }
.toList()

private const val DEFAULT_CAPACITY = 5
}

fun cacheFactory(capacity: Int): Cache<String, Int>

fun testKeysCaching() {
val cache = cacheFactory(DEFAULT_CAPACITY)

var i = 0

while (i < DEFAULT_CAPACITY) {
assertEquals(i, cache.size)
assertEquals(Optional.none(), cache.set(ITEMS[i].first, ITEMS[i].second))
i++
for (j in 0 until i) {
assertEquals(Optional.some(ITEMS[j].second), cache[ITEMS[j].first])
}
for (j in i until ITEMS.size) {
assertEquals(Optional.none(), cache[ITEMS[j].first])
}
}

while (i < ITEMS.size) {
assertEquals(DEFAULT_CAPACITY, cache.size)
assertEquals(Optional.some(ITEMS[i - DEFAULT_CAPACITY]), cache.set(ITEMS[i].first, ITEMS[i].second))
i++
for (j in 0 until (i - DEFAULT_CAPACITY)) {
assertEquals(Optional.none(), cache[ITEMS[j].first])
}
for (j in (i - DEFAULT_CAPACITY) until i) {
assertEquals(Optional.some(ITEMS[j].second), cache[ITEMS[j].first])
}
for (j in i until ITEMS.size) {
assertEquals(Optional.none(), cache[ITEMS[j].first])
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package it.unibo.tuprolog.utils

class CursorTest {
}
12 changes: 12 additions & 0 deletions core/src/commonTest/kotlin/it/unibo/tuprolog/utils/LRUCacheTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package it.unibo.tuprolog.utils

import it.unibo.tuprolog.utils.impl.LRUCache
import kotlin.test.Test

class LRUCacheTest : CacheTest by CacheTest.prototype(::LRUCache) {

@Test
override fun testKeysCaching() {
super.testKeysCaching()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package it.unibo.tuprolog.utils

import it.unibo.tuprolog.utils.impl.SimpleLRUCache
import kotlin.test.Test

class SimpleLRUCacheTest : CacheTest by CacheTest.prototype(::SimpleLRUCache) {

@Test
override fun testKeysCaching() {
super.testKeysCaching()
}
}

0 comments on commit 05656ed

Please sign in to comment.