Giter VIP home page Giter VIP logo

stateless's Introduction

Stateless

Join the chat at https://gitter.im/hablapps/stateless

Overview

Optics provide abstractions and patterns to access and update immutable data structures. They compose, both homogeneously and heterogeneously, so they become essential to express complex data transformations in a modular and elegant way. However, optics are restricted to work solely with in-memory data structures.

Stateless is a type class based framework that provides the means to take optic awesomeness to new settings, such as databases or microservices. To do so, it exploits optic algebras, an abstraction that generalizes monomorphic optics and enables programmers to describe the data layer and business logic of their applications in abstract terms, while keeping them completely decoupled from particular infrastructures.

Set Up

Stateless is currently available for Scala 2.12, for the JVM platform.

To get started with SBT, simply add the following to your build.sbt file.

resolvers ++= Seq("Habla releases" at "http://repo.hablapps.com/releases")

libraryDependencies += "org.hablapps" %% "stateless" % "0.1"

(!) Soon, we'll publish this library in official repositories, so the resolver will become unnecessary.

This library depends on Monocle, Scalaz and Shapeless.

Getting Started

Suppose that we wanted to modify the optional zip code field associated to a person, which in turn belongs to a certain department. We clearly identity three entities here: department, person and address. We could implement this logic with case classes and Monocle as follows:

import monocle.function.Each._
import monocle.macros.Lenses
import monocle.std.option.some

// Data Layer
@Lenses case class SDepartment(
  budget: Long,
  people: List[SPerson])

@Lenses case class SPerson(
  name: String,
  address: Option[SAddress])

@Lenses case class SAddress(
  city: String,
  zip: Int)

// Business Logic
def modifyZip(f: Int => Int): SDepartment => SDepartment = {
  import SDepartment.people, SPerson.address, SAddress.zip
  (people composeTraversal each 
          composeLens address 
          composePrism some 
          composeLens zip).modify(f)
}

The resulting implementation for modifyZip, while expressing a complex transformation, is modular and readable. However, we're constrained to access and mutate in-memory data structures. If we wanted to persist the state of the application in a relational database, we should discard optics in favor of the specific transformations provided by Slick, Doobie, or any other database framework. Otherwise, we'd need to pull the whole state, modify it by means of an optic and finally put it back again, which turns out to be impractical.

Stateless provides the means to describe the data layer of backend applications in a decoupled way, exploiting the algebra and modularity from optics, and later instantiate it to in-memory data structures, relational databases or any other effectful state-based framework.

Decoupled Data Layer

This is the only import that we're gonna need for this example:

import stateless.core.nat._

Stateless adopts type classes to implement application entities. This is how we encode Address:

trait Address[Ad] {
  type P[_]

  val city: LensAlg[P, String]
  val zip: LensAlg[P, Int]
}

As you can see, an address is any type Ad for which we can provide two lenses city and zip. These fields allow us to access and modify the city and zip codes of an address Ad, respectively. The particular type of transformation program that it will actually be employed to access and transform addresses is abstracted away by type constructor P. A typical instantiation for P is a state-based transformation State[Ad,?], in case that we want to store our application state using case classes. Alternatively, if the application state is stored in a relational database, we will typically use a reader-like program, where the read only state refers to the identifier of the address and the configuration of the database server.

The second entity is Person, that contains a name and optionally an address. It's represented this way:

trait Person[Pr] {
  type P[_]
  type Ad; val Address: Address[Ad]

  val name: LensAlg[P, String]
  val optAddress: OptionalAlg.Aux[P, Address.P, Ad]
}

As before, this entity takes a type parameter Pr , which represents the entity state, and a type member P, that corresponds with the program that evolves it. As this entity contains a name and an address, we declare two optic algebras: name and optAddress. There is nothing remarkable about name, but optAddress, which is a so-called optional algebra, brings new patterns.

Specifically, its returning type refers to a generic address type Ad, which is declared as a type member. The fact that we intend this type to be used as an actual address is declared through the corresponding type class instance Address. Also, the optAddress field's type exposes a second type constructor parameter through the Aux pattern. In fact, every optic algebra hides a type constructor member Q, that represents the type of program that evolves the focus; optic algebras are then equipped with a natural transformation that turns these inner programs into programs that evolve the whole P, or outer programs. Since the focus of the optional field is of type Ad, we use Address.P as the type of inner programs for the optional lens. This is the essential mechanism that enables optic algebra composition in stateless.

Finally, this is how we represent departments:

trait Department[Dp] {
  type P[_]
  type Pr;  val Person: Person[Pr]

  val budget: LensAlg[P, Long]
  val people: TraversalAlg.Aux[P, Person.P, Pr]
}

Besides the new traversal algebra for the people field, there's nothing new in this entity definition. So, our data layer is fully defined!

(!) Don't be intimidated by the accidental complexity: type members, nested entity evidences, etc.. By now, you can simply follow this pattern. We're working hard on hiding those aspects, to make the implementation of data layers through stateless as close as possible to the definition of the corresponding case class.

Decoupled Business Logic

Once we have defined our data layer, it's time for us to implement the business logic. This is how we modify the zip codes for all the members of the department in a declarative way:

def modifyZip[D](f: Int => Int)(Dep: Department[D]): Dep.P[Unit] = {
  import Dep.people, Dep.Person.optAddress, Dep.Person.Address.zip
  (people composeOptional optAddress composeLens zip).modify(f)
}

First, compare to the modifyZip function for case classes that was shown previously, this new signature is truly generic. It no longer works for a specific SDepartment case class, but for any type D that qualifies as a department. Similarly, the type of the transformation program returned by this function is not fixed once and for all but depends on the actual type of department received. This level of generality allows us to decouple this implementation from any concrete infrastructure. And, still, what is great here is that this implementation is almost the same as the one we did for the in-memory scenario with Monocle.

Recovering the In-memory Setting

Now, if we want to do something useful with our data layer, we need to instantiate its implementation in the effectful land. In order to show that our approach generalizes classic optics, we provide an in-memory instantiation bellow:

import smonocle.nat.all._

val stateDepartment = new Department[SDepartment] {
  type P[X] = State[SDepartment, X]

  type Pr = SPerson
  val Person = new Person[Pr] {
    type P[X] = State[Pr, X]

    type Ad = SAddress
    val Address = new Address[Ad] {
      type P[X] = State[Ad, X]

      val city = asLensAlg(SAddress.city)
      val zip = asLensAlg(SAddress.zip)
    }

    val name = asLensAlg(SPerson.name)
    val optAddress = asOptionalAlg(SPerson.address composePrism some)
  }

  val budget = asLensAlg(SDepartment.budget)
  val people = asTraversalAlg(SDepartment.people composeTraversal each)
}

If we feed this value to modifyZip, we get a State[SDepartment, Unit] program, which updates all the zip codes existing in the department when executed over an instance of SDepartment.

scala> val initial = SDepartment(1000, List(
     |   SPerson("Juan", Some(SAddress("Leganes", 28911))),
     |   SPerson("Maria", Some(SAddress("Mostoles", 28934)))))
initial: org.hablapps.stateless.test.SDepartment = ...

scala> modifyZip(_ + 1)(stateDepartment).exec(initial)
res0: org.hablapps.stateless.test.SDepartment = SDepartment(1000,List(SPerson(Juan,Some(SAddress(Leganes,28912))), SPerson(Maria,Some(SAddress(Mostoles,28935)))))

We can see how the zip codes for Juan and Maria are incremented in one unit.

Relational Database Instance

We provide facilities to instantiate the data layer of an application with doobie. In this sense, each kind of optic has an associated schema. The programmer is responsible for supplying instances of such schemas to build the corresponding optic algebras. In general, these ideas are work in progress, you can check the standing facilities here.

Additional Features

Stateless provides additional features that we find interesting for many implementations:

  • It contains indexed optic algebras, which turn out to be very handy when dealing with entities whose parts are indexed.
  • It provides typeclasses that produce optic algebras, such as At and FilterIndex. They correspond with which is known as optic functions in Monocle.
  • Stateless includes recurrent combination of optics. For example, by packaging a collection of lenses and traversals under certain configuration, we are able to implement the interface of a Scala Map, which is very intuitive from an object-oriented mindset.
  • The library offers utilities to facilitate the instantiation of relevant frameworks in the Scala ecosystem.

Limitations

Stateless is still very experimental, and therefore comes with some limitations.

  • We've said that optic algebras generalize optics. However, we have to specify here that we're generalizing the monomorphic version of optics, instead of the polymorphic version identified by Russell O'Connor which leads to the classic STAB.
  • Our traversal algebra is not really a generalization of a traversal. In fact, it generalizes what we have considered a weak traversal, more restricted but good enough for most of cases.
  • We haven't introduced Adapters or Prisms yet. These optics are special since they are able to build the whole from the part without additional help, meaning that they don't need to access the current state to evolve the entity. Therefore, we still have to determine if they make sense in this context.
  • We need to establish what we consider "reasonable" when we talk about performance. The optimization of instances is crucial to make stateless viable.
  • We're working on macro annotations to remove boilerplate from applications.

License

Stateless is licensed under the Apache License, Version 2.0.

stateless's People

Contributors

gitter-badger avatar javierfs89 avatar jserranohidalgo avatar neutropolis avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

stateless's Issues

Utilities to test laws

Each optic algebras comes along with the laws that it should hold. We provide a ScalaCheck hook to test them, but it turns to be useless, since generating the evidences that it requires is quite a challenge. For example, we should be able to create Equal evidences for StateT programs. This task consists on analyzing these aspects and determine if this is viable. Perhaps, part of the functionality that we need would be contemplated in puretest.

Facilities to deal with `State` instance

We provide some utilities to create optic algebras from monocle optics. However, they were created in an ad-hoc way, to fulfill the needs from the applications that we were working on (geofences, zipcode, etc.). It'd be fine if we could determine the fundamental utilities that users are going to need.

Refine Doobie instance

The facilities to deal with Doobie are very experimental. Thereby, we need to determine the optimal way to map our data layer into this technology.

Documentation (code & wiki)

We need to provide further documentation (beyond the README) to make the library approachable. This includes documentation in code (right now it's practically inexistent) and as a wiki book.

Reduce data layer accidental complexity

When defining an association between entities, we need something like this:

trait Person[Pr] {
  type P[_]
  type Ad; val Address: Address[Ad]
  val optAddress: OptionalAlg.Aux[P, Address.P, Ad]
}

This is very noisy. One of the ideas that we'd like to apply is the next one:

trait Person[Pr] {
  type P[_]
  type Ad: Address
  val optAddress: OptionalAlg.Aux[P, Ad.P, Ad]
}

However, at first glance, it seems to be impossible to implement this idea with macros. Is there any other way to get something similar to this?

Consider unifying `op` and `lib`

Does it make sense to place MapAlg and At in the same folder? What are the differences between the abstractions located in op and the ones located in lib to keep them separated?

Integration with *cats*

Now, we're using scalaz exclusively. Eventually, adapting stateless to cats will be necessary.

At, FilterIndex and Indexed Optics

At is returning a plain lens algebra, while FilterIndex is returning indexed traversals. This is due to ad-hoc requirements by our testing applications. We should be more homogeneous here, by either:

  • Creating plain and indexed versions for each op
  • Creating only an indexed version for each op (and ignoring the index if not necessary)

Remove `OpticField` from `State` facilites

LensField, OptionalField, etc. are terrible names. Besides, they seem to be no longer useful, since we can use OpticAlg[P, A] instead (notice that we're ignoring the type Q) for the scenarios where fields are useful.

Doobie Mapping Performance

The performance of the standing doobie mapping is not good, since we're doing too many accesses to the database. In order to reduce them, we're probably gonna need Free representations, to inspect the programs and optimize them before launching the missiles.

Indexed Optics Fundamentals

Our current indexed optic fundamentals are not very solid. We should take a look at the state-of-the-art of indexed optics to check its validity. By now, they work for our main objectives (mainly maps).

Functional dependency between `P[_]` and `A`

We define our entities in the data layer this way:

trait Entity[A] {
  type P[_]
}

We know that P[_] and A are related, since P is somehow hiding a value of type A. However this connection isn't reflected at all in the code. This notion seems like a functional dependency in Haskell, but we think that it should be enough if we define a MonadState[A, P] inside the trait. Consider this alternative and apply it, if useful.

Publish library in official repos

Right now, we're publishing stateless in our own (Habla Computing) repository. This is producing warnings on SBT 0.13, and it doesn't even work for SBT 1.0. Since the project is public now, we should publish the library in official repositories.

Abstracting away from optics

Hiding optic idioms in favor of more standard names, such as attribute, association, etc. This coq snippet is just a proof of concept.

Record one_to_one (p : Type -> Type) `{Monad p}
                  (record : forall (q : Type -> Type), Monad q -> Type) :=
{ q : Type -> Type
; A : Type
; M : Monad q
; MS : MonadState A q
; ev : record q _
; hom : lensAlgHom p q A
}.
Arguments hom [p _ record].
Arguments ev [p _ record].

Definition attribute := lensAlg'.

Notation "a ▷ f " := (f a) (at level 40, left associativity).

Record Address (p : Type -> Type) `{Monad p} := mkAddress
{ _zip  : attribute p nat
; _city : attribute p string
}.
Arguments _zip [p _].
Arguments _city [p _].

Record Person (p : Type -> Type) `{Monad p} := mkPerson
{ _name    : attribute p string
; _address : one_to_one p Address
}.
Arguments _name [p _].
Arguments _address [p _].

Definition address {p} `{Monad p} (data : Person p) :=
  hom (_address data).

Definition zip {p} `{Monad p} {data : Person p} :=
  _zip (ev (_address data)).

Definition modifyPersonZip {p} `{Monad p}
                           (f : nat -> nat)
                           (data : Person p) : p unit :=
  (data ▷ address • zip) ~ f.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.