typedmap is an implementation of heterogeneous type-safe map pattern in Kotlin

Overview

Typedmap

typedmap is an implementation of heterogeneous type-safe map pattern in Kotlin. It is a data structure similar to a regular map, but with two somewhat contradicting features:

  • Heterogeneous - it can store items of completely different types (so this is like Map).

  • Type-safe - we can access the data in a type-safe manner and without manual casting (unlike Map).

To accomplish this, instead of parameterizing the map as usual, we need to parameterize the key. Keys are used both for identifying items in the map and to provide us with the information about the type of their associated values.

As this is much easier to explain and understand by looking at examples, we will go straight to the code!

Examples

Get by Type

Probably the most common example of a similar data structure is an API where we provide a Class to get an instance of it. In Java, it could look like this:

public <T> T get(Class<T> cls)

Guava’s ClassToInstanceMap is a good example of such a data structure. Additionally, there are e.g. EntityManager.unwrap() and BeanManager.getExtension() methods that utilize similar API, however, their purpose is more specialized.

typedmap supports this feature with a clean API:

() println("User: $user") // User(username=alice)">
// create a typed map
val sess = simpleTypedMap()

// add a User item
sess += User("alice")

// get an item of the User type
val user = sess.get<User>()

println("User: $user")
// User(username=alice)

Due to advanced type inferring in Kotlin, in many cases we don’t need to specify a type when getting an item:

fun processUser(user: User) { ... }

processUser(sess.get()) // Works as expected
fun getUser(): User {
    return sess.get() // Works as expected
}

typedmap fully supports parameterized types:

: ${sess.get>()}") // [1, 2, 3, 4, 5] println("List: ${sess.get>()}") // [a, b, c, d, e]">
sess += listOf(1, 2, 3, 4, 5)
sess += listOf("a", "b", "c", "d", "e")

println("List: ${sess.get>()}")
// [1, 2, 3, 4, 5]
println("List: ${sess.get>()}")
// [a, b, c, d, e]
Note
SimpleTypedMap, which we use here, does not support polymorphism. Both get>() and get>() would not find a requested item and throw an exception. Polymorphism could be supported by more advanced implementations of TypedMap.

Get by Key

Looking for items by their type is very convenient, but in many cases this is not enough. For example, it is difficult to store multiple instances of the same class and access them individually. Moreover, if we need to store an item of a common type, e.g. String, then the code get() becomes enigmatic, because it is not clear what is the String we requested. In such cases, we can create keys to identify items in the map.

Let’s assume we develop a web application, and we store some data in the web session. In the previous example, we stored a user object in the session, but this time we just need to store a username. In addition, we would like to store a session ID and visits count. We have to create a key for each item and use these keys to identify values:

object Username : TypedKey<String>()
object SessionId : TypedKey<String>()
object VisitsCount : TypedKey<Int>()

sess[Username] = "alice"
sess[SessionId] = "0123456789abcdef"
sess[VisitsCount] = 42

println("Username: ${sess[Username]}")
// "alice"
println("SessionId: ${sess[SessionId]}")
// "0123456789abcdef"
println("VisitsCount: ${sess[VisitsCount]}")
// 42

When creating a key, we need to provide a type of its associated value. This makes possible to provide a fully type-safe API:

val username = sess[Username] // type: String
val visits = sess[VisitsCount] // type: Int

sess[Username] = 50 // compile error

Similarly as in the previous section, we can use keys in conjunction with parameterized types:

object UserIds : TypedKey<List<Int>>()
object Labels : TypedKey<List<String>>()

sess[UserIds] = listOf(1, 2, 3, 4, 5)
sess[Labels] = listOf("a", "b", "c", "d", "e")
sess[Labels] = listOf(1, 2, 3, 4, 5) // compile error

println("UserIds: ${sess[UserIds]}")
// [1, 2, 3, 4, 5]
println("Labels: ${sess[Labels]}")
// [a, b, c, d, e]

Key With Data

Declaring keys in the way described above is fine if we need to store a finite set of known items, so we can create a distinct key for each of them. In practice though, we very often need to create keys dynamically and store an arbitrary number of items in a map. This is supported by typedmap as well, and we still keep its type-safety feature. In fact, this case is implemented in typedmap in a very similar way to regular maps.

Instead of creating the key as a singleton object, we need to define it as a class. hashCode() and equals() have to be properly implemented, so the easiest is to use a data class:

// value
data class Order(
    val orderId: Int,
    val items: List<String>
)

// key
data class OrderKey(
    val orderId: Int
) : TypedKey()

sess[OrderKey(1)] = Order(1, listOf("item1", "item2"))
sess[OrderKey(2)] = Order(2, listOf("item3", "item4"))

println("OrderKey(1): ${sess[OrderKey(1)]}")
// Order(orderId=1, items=[item1, item2])
println("OrderKey(2): ${sess[OrderKey(2)]}")
// Order(orderId=2, items=[item3, item4])

This example could be improved by using the AutoKey util. AutoKey is a very simple interface that we can implement to make map items responsible for creating their keys:

data class Order(
    val orderId: Int,
    val items: List<String>
) : AutoKey {
    override val typedKey get() = OrderKey(orderId)
}

sess += Order(1, listOf("item1", "item2"))
sess += Order(2, listOf("item3", "item4"))
Note
You could notice that we used plusAssign() operator (+=) earlier, and it had a different meaning. This is true, sess += Order() could be interpreted both as "set by autokey" (so the key is OrderKey object) or as "set by type" (key is similar to Class). By default, objects implementing AutoKey are stored by autokey, which is probably what we really need. To store autokey objects by their type, we need to use setByType() function explicitly.

Installation

Add a following dependency to the gradle/maven file:

build.gradle
dependencies {
    implementation "me.broot.typedmap:typedmap-core:${version}"
}
build.gradle.kts
dependencies {
    implementation("me.broot.typedmap:typedmap-core:${version}")
}
pom.xml
<dependency>
    <groupId>me.broot.typedmapgroupId>
    <artifactId>typedmap-coreartifactId>
    <version>${version}version>
dependency>

Replace ${version} with e.g.: "1.0.0". The latest available version is: Maven Central

Now, we can start using typedmap:

val map = simpleTypedMap()

Building

To build the project from sources, run the following command:

Linux / macOS
$ ./gradlew build
Windows
gradlew.bat build

After a successful build, the resulting jar file will be placed in:

  • typedmap-core/build/libs/typedmap-core.jar

Use Cases

Some people may ask: what do we need this for? Or even more specifically: how is the typed map better than just a regular class with known and fully typed properties? Well, in most cases it is not. However, there are cases where such a data structure could be very useful.

Sometimes, we need to separate the code responsible for providing a data storage and the code storing its data there. In such a case, the first component knows nothing about the data it stores, so the data container can’t be typed easily. Often, it is represented as Map, Map or just Any/Object.

Examples:

  • Session data in web frameworks - framework provides the storage, web application uses it.

  • Request/response objects in web/network frameworks - they often contain untyped data storage, so middleware or application developer could attach additional data to request/response.

  • Applications with support for plugins - plugins often need to store their data somewhere and application provides a place for it.

  • Data storage shared between loosely coupled modules.

    Let’s assume we develop some kind of data processing software. Our data processing is very complex, so we divided the whole process into several smaller tasks and organized the code into clean architecture of multiple packages or even separate libraries. Modules produce results of their processing and may consume results of other modules, so we need a central cache for storing these results.

    The problem is: central cache needs to know data structures of all available modules, so we partially lose benefits of our clean design. It is even worse if modules are provided as external libraries.

  • Objects designed to be externally extensible, i.e. by other means than subtyping. We can easily add new behavior to a class by extension or static functions, but we can’t add any additional data fields to it. Similar example are classes allowing to attach hooks to affect their behavior.

  • Separation of concerns. Often, we divide the code of our application into a utility of generic usage and a code related to an application logic. In such a case we don’t want to pollute utility classes with an application logic, but sometimes we still need to somehow reference application objects from utility classes. Usually, it can be solved with generics though.

Above cases aren’t very common, some of them are rather rare. Still, it happens from time to time. Generally speaking, whenever we design or use a class which owns a property like Any or Map with contract like: "Put there any data you need, it won’t be modified, but just kept for you", the typed map structure could be potentially useful. Such properties are often named "extras", "extra data", "properties", etc.

Real-World Examples

There are several existing examples in Java with similar requirements to described above and implemented using either untyped container and manual casting or with a class-to-instance map or function:

Additionally, there are examples of data structures very similar to typedmap. In fact, they exist in one of the most popular libraries for Kotlin:

  • CoroutineContext - its Key interface and get() function. It allows to store any data within a coroutine.

  • Attributes of Ktor web framework by JetBrains. It is used as a storage for middleware.

Alternatives

Typed maps aren’t the only solution to a similar problem. There are other techniques, including:

  • Use untyped map (e.g. Map) as a central storage and provide strongly-typed accessors by clients/modules. Accessors could be: extension functions, static functions or even classes that wrap untyped map and provide an easy to use API.

    This solution could be very convenient to use, especially with extension functions, however, writing accessors requires much more work than just creating a typed key. Furthermore, typedmap naturally guarantees that each key is unique. Accessors need to do the same or they would risk conflicts.

  • class-to-instance maps.

    In many cases they are less convenient to use. For example, if we need to store multiple simple items (strings, integers), we need to create a wrapper class for each of them and then wrap/unwrap a value whenever storing/retrieving it. Also, it is not trivial to store collections of items as in Key With Data.

You might also like...
Type-safe time calculations in Kotlin, powered by generics.

Time This library is made for you if you have ever written something like this: val duration = 10 * 1000 to represent a duration of 10 seconds(in mill

Kotlin code generation for commercetools platform type-safe product-types, reference expansion and custom fields

Kotlin code generation for commercetools platform type-safe product-types, reference expansion and custom fields

A view abstraction to provide a map user interface with various underlying map providers
A view abstraction to provide a map user interface with various underlying map providers

AirMapView AirMapView is a view abstraction that enables interactive maps for devices with and without Google Play Services. It is built to support mu

This is a repo for implementing Map pan or drag (up, down, right ,left) to load more places on the Google Map for Android

Challenge Display restaurants around the user’s current location on a map ○ Use the FourSquare Search API to query for restaurants: https://developer.

Map-vs-list-comparator - The project compares the time needed to find a given element in a map vs the time needed to find a given element in a list.

Map vs List Comparator The project compares the time needed to find a given element in a map vs the time needed to find a given element in a list. To

A type-safe HTTP client for Android and the JVM

Retrofit A type-safe HTTP client for Android and Java. For more information please see the website. Download Download the latest JAR or grab from Mave

Type safe intent building for services and activities

#IntentBuilder Type safe intent building for services and activities. IntentBuilder is a type safe way of creating intents and populating them with ex

The sample App implements type safe SQL by JOOQ & DB version control by Flyway

The sample App implements type safe SQL by JOOQ & DB version control by Flyway Setup DB(PostgreSQL) $ docker compose up -d Migration $ ./gradlew flywa

Type-safe arguments for JetPack Navigation Compose using Kotlinx.Serialization

Navigation Compose Typed Compile-time type-safe arguments for JetPack Navigation Compose library. Based on KotlinX.Serialization. Major features: Comp

SimpleApiCalls is a type-safe REST client for Android. The library provides the ability to interact with APIs and send network requests with HttpURLConnection.

SimpleApiCalls 📢 SimpleApiCalls is a type-safe REST client for Android. The library provides the ability to interact with APIs and send network reque

Kotlin extension function provides a facility to "add" methods to class without inheriting a class or using any type of design pattern

What is Kotlin Extension Function ? Kotlin extension function provides a facility to "add" methods to class without inheriting a class or using any ty

compaKTset is a small library aimed at providing you with the most memory efficient Set implementation for any particular data type of your choosing.

compaKTset is a small library aimed at providing you with the most memory efficient Set implementation for any particular data type of your choosing.

Saga pattern implementation in Kotlin build in top of Kotlin's Coroutines.

Module Saga Website can be found here Add in build.gradle.kts repositories { mavenCentral() } dependencies { implementation("io.github.nomisr

An implementation of the Bloc pattern for Kotlin and Jetpack Compose

Kotlin Bloc An implementation of the Bloc pattern for Kotlin and Jetpack Compose. Documentation Documentation is available here: https://ptrbrynt.gith

Implementation of
Implementation of "Side Navigation" or "Fly-in app menu" pattern for Android (based on Google+ app)

Android SideNavigation Library Implementation of "Side Navigation" or "Fly-in app menu" pattern for Android (based on Google+ app). Description The Go

Kreds - a thread-safe, idiomatic, coroutine based Redis client written in 100% Kotlin

Kreds Kreds is a thread-safe, idiomatic, coroutine based Redis client written in 100% Kotlin. Why Kreds? Kreds is designed to be EASY to use. Kreds ha

TradeMap-Clone - Trade Map Clone with kotlin
TradeMap-Clone - Trade Map Clone with kotlin

TradeMap-Clone APP que simula atualização da bolsa de valores em tempo real para

An Android instance app for working with Google Map, Kotlin

map-instant-app An Android instance app Android technologies that I used: Name Description 1 Kotlin 2 Coroutine 3 Navigation 4 DataBinding 5 ViewBindi

An Android instance app for working with Google Map, Kotlin
An Android instance app for working with Google Map, Kotlin

map-instance-app A map instance app for seeing the current position of the user and saving that in the database and showing a list of saved locations.

Comments
  • Support Map-like

    Support Map-like "computeIfAbsent" method

    For reference: https://docs.oracle.com/javase/8/docs/api/java/util/Map.html#computeIfAbsent-K-java.util.function.Function-

    Side note: there are a bunch of other methods in the Map class, such as computeIfPresent, merge, putIfAbsent, etc., that seem to be missing, but I'm not sure if every method carries over cleanly to the TypedMap concept. Still, it may be worth auditing all the methods in the Map API to double-check if they can be migrated to TypedMap.

    computeIfAbsent is really useful and I use it all the time. It's great for lazy initialization.

    For example, maybe I want to record a set of seen ID values, but most people may not use the feature that registers them.

    Right now, if I understand it, you'd have to do something like:

    sess += mutableSetOf<String>()
    
    ... later ...
    fun registerId(id: String) {
       val ids = sess.get<MutableSet<String>>()
       require(!ids.containsKey(id)) { "Duplicate id registered: $id" }
       ids.add(id)
    }
    

    OR

    object IdKey : TypedKey<MutableSet<String>>()
    sess[IdKey] = mutableSetOf<String>()
    
    ... later ...
    fun registerId(id: String) {
       val ids = sess[IdKey]
       ...
    }
    

    With computeIfAbsent, I can use this pattern, which is both fewer lines and only instantiates the set if I need it:

    fun registerId(id: String) {
       val ids = sess.computeIfAbsent { mutableSetOf<String>() }
       require(!ids.containsKey(id)) { "Duplicate id registered: $id" }
       ids.add(id)
    }
    

    OR

    ... later ...
    fun registerId(id: String) {
       val ids = sess.computeIfAbsent(IdKey) { mutableSetOf<String>() }
       ...
    }
    

    If there's a way to do this with the existing APIs that I missed, my apologies!

    opened by bitspittle 1
  • Make the library compatible with Java 8 projects

    Make the library compatible with Java 8 projects

    When using in projects configured like this:

    java {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
    

    It throws errors:

    Incompatible because this component declares an API of a component compatible with Java 14 and the consumer needed a runtime of a component compatible with Java 8

    opened by broo2s 0
Owner
Ryszard Wiśniewski
Ryszard Wiśniewski
This is a repo for implementing Map pan or drag (up, down, right ,left) to load more places on the Google Map for Android

Challenge Display restaurants around the user’s current location on a map ○ Use the FourSquare Search API to query for restaurants: https://developer.

Mahmoud Ramadan 7 Jul 30, 2022
This project allows you to calculate the route between two locations and displays it on a map.

Google-Directions-Android This project allows you to calculate the direction between two locations and display the route on a Google Map using the Goo

Joel Dean 970 Dec 15, 2022
malik dawar 87 Sep 18, 2022
LocationPicker 2.1 0.4 Java - A simple and easy to way to pick a location from map

Introduction LocationPicker is a simple and easy to use library that can be integrated into your project. The project is build with androidx. All libr

Shivpujan yadav 61 Sep 17, 2022
My Maps displays a list of maps, each of which show user-defined markers with a title, description, and location. The user can also create a new map. The user can save maps and load them in from previous usages of the app.

My Maps Bryant Jimenez My Maps displays a list of maps, each of which show user-defined markers with a title, description, and location. The user can

null 0 Nov 1, 2021
Slime loader is a map loader & saver for the file format Slime as specified here implemented in Minestom.

??️ SlimeLoader Slime loader is a map loader & saver for the file format Slime as specified here implemented in Minestom. Features: World loading Bloc

null 25 Oct 2, 2022
Self-hosted map of visited places (with a very fancy stack underneath)

Tripmap Self-hosted map of visited places (with a very fancy stack underneath) Features Sharing custom map-view with pins in locations marked by user.

Igor Kurek 2 Feb 6, 2022
Tree View; Mind map; Think map; tree map

A custom tree view for Android, designed for easy drawing some tree nodes (e.g. thind mind and tree nodes). Includes smoothly zoom, move, limit and center fix animation support, and allows easy extension so you can add your own child node's customs view and touch event detection.

怪兽N 304 Jan 3, 2023
The app has got fullscreen Turkey map via Huawei Map. App selects random province and shows it borders on the map than user will try to guess the provinces name.

Il Bil App Introduction I will introduce you to how to implement Account Kit, Map Kit, Game Service. About the game: The app has got fullscreen Turkey

Gökhan YILMAZ 4 Aug 2, 2022
💡🚀⭐️ A generalized adapter for RecyclerView on Android which makes it easy to add heterogeneous items to a list

Mystique is a Kotlin library for Android’s RecyclerView which allows you to create homogeneous and heterogeneous lists effortlessly using an universal

Rahul Chowdhury 48 Oct 3, 2022