FTypes:
- Right now: IS EXPERIMENTAL
- Is a type system (that is: a collection of base types plus means for building complex types out of simpler ones)
- Is for concurrent programming (async)
- Solve the same problem than futures do. But without futures (or nearly so)
- Allows working with asynchronous types as if they were normal synchronous ones
- These async types have the same interface (methods) than their synchronous counterparts
import scala.concurrent.Future
import com.bryghts.ftypes._
val a = async.Int(Future.successful(3)) // Convert Future into async
val b = async.Int(Future.successful(4))
val c: async.Int = a + b
Include in your SBT file:
addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full)
libraryDependencies += "com.bryghts.ftypes" %% "ftypes" % "0.0.3"
Or, if you are using ScalaJS:
addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full)
libraryDependencies += "com.bryghts.ftypes" %%% "ftypes" % "0.0.3"
NOTE: The first line is to include the Macro Paradise compiler plugin which powers the @Async macro-annotation
Imagine we have coded a function that returns how many healthy and active servers we have, for a particular region. The response of such a method would quite possibly be asynchronous, which, if we want to access the value, usually opens up three possibilities:
- Blocking. Currently, in the Scalasphere this is considerd a Very Bad Idea (I'm looking on sources to support this point)
- Callbacks. Nearly as bad a blocking (Google for "Callback Hell")
- Futures (of which FTypes are a variant)
In this example, I'm going to compare standard scala Futures with FTypes.
In this case, our method would have a signature more or less like this:
import scala.concurrent._
def getActiveServers(regionId: Int): Future[Int] = ???
And, if we want to compute how many servers we have if we combine Europe and America:
val europeCount: Future[Int] = getActiveServers(europeId)
val americaCount: Future[Int] = getActiveServers(americaId)
val totalCount: Future[Int] =
europeCount.flatMap(e => americaCount.map(a => e + a))
Or, with for comprehencion, we can make it a bit nicer:
val europeCount: Future[Int] = getActiveServers(europeId)
val americaCount: Future[Int] = getActiveServers(americaId)
val totalCount: Future[Int] = for {
e <- europeCount
a <- americaCount
} yield a + e
Here, our method would have a really similar signature:
import com.bryghts.ftypes._
def getActiveServers(regionId: Int): async.Int = ???
The real difference comes when we use this method to the same computation than before:
val europeCount: async.Int = getActiveServers(europeId)
val americaCount: async.Int = getActiveServers(americaId)
val totalCount: async.Int = europeCount + americaCount
async.Int is just one of the types that FTypes provides out of the box, with, practically, all the same operations (methods) than the standard Int, where, like in the example, the '+' operation returns an async.Int that will hold the value of the sum (whenever the other two numbers are available)
Most Scala devs have to work with Futures in a daily basis and know the toll they apply in the mental model and the code readability/maintainability. If you don't know what a Future is, you can find an excellent introduction in this article by Daniel Westheide.
If you know what a future is and how it works, let me present you with a comparison. On one side, we have:
Future[Int]
On the other hand, we have:
async.Int
Both represent exactly the same thing, an Int value that will be available at some, unknown, point in time.
Comparing both snippets, you can draw an immediate conclusion. While a Future is a generic type (a Future can hold any type of data), the async.Int
is a concrete type that con only hold integers. This means that if you want some sort of asynchronous Boolean
, with Futures you have the problem already solved (i.e. Future[Boolean]
) while with FTypes you need another specific type (which, by the way, FTypes already provides and is called async.Boolean
).
The main difference comes when you compare the methods provided by Future
and async.Int
. Future is a generic type and knows nothing about the data is going to end up holding, were async.Int is always going to be used to only hold ints. In fact async.Int
has none of the methods provided by the Future
class but (nearly) all the methods provided by a regular, synchronous Int
.
That is: With an async.Int
you operate exactly the same as if it was a normal scala.Int
And this has a nice consequence: Functions that before received regular, synchronous types:
def average(a: Double, b: Double): Double = (a + b) / 2
Can now be reimplemented for the asynchronous world like this:
def average(a: async.Double, b: async.Double): async.Double = (a + b) / 2
FTypes is a type system. What I mean with that is that it provides a collection of base-types and means for creating complex types out of simpler ones, like async arrays or async case classes.
In the end, the async types provided by FTypes are a very specialised form of Future: something that will hold a value at some point in time, in the future. This something, this result of a computation, is going to be a concrete series of bits in your computer. And this series of bits, to be meaningful, are going to have a concrete, synchronous, old-school type.
If, for example, a method returns an async.Int
, what we are saying is that this method is returning a ticket, a special ticket that is changeable for the actual value when that value becomes available. But, that value that will become available is going to be an standard Scala Int
.
The async.Int
type is associated with (or represented by) an scala.Int
And that holds true for every single async type.
All async types extend:
async.Any[T, FT]
Where, T
is the synchronous type and FT
is the type extending async.Any
. For example, async.Int
declared in a way similar to this:
package com.bryghts.ftypes
package async
class Int extends async.Any[scala.Int, async.Int]
For now, the following base types are implemented:
- async.Byte
- async.Char
- async.Short
- async.Int
- async.Long
- async.Float
- async.Double
- async.Boolean
- async.String
Note: The RichInt and similar extensions have not been implemented yet.
You can create immutable async arrays like with basic arrays:
val a = async.Array[async.Int](1, 2, 3)
val b = a(1)
val l = l.length // returns an async.Int
These arrays are immutable because update operations on asynchronous types would break the reason why futures work so well.
NOTE: async arrays can only hold async types
This bit is extremely experimental, because:
- It uses macro annotations (which are experimental by themselves)
- Macros are hard and complex to write, which makes this bit require a lot more testing than it has (for now)
As I've mentioned before, every async type needs a synchronous counterpart. And this starts being a problem when you need to create custom classes.
Imagine we need a Person class:
case class Person(name: String, age: Int)
If we want to make "Person" asynchronous, the first idea that comes to mind, is to make the fields asynchronous:
case class Person(name: async.String, age: async.Int)
But this doesn't really solve the problem. If you have a method that returns a Person in an async way you will still need to return a Future[Person], which doesn't solve much. Another option is to create two types and relate them. This is a non-compiling and simplified version on how you would do that with FTypes:
case class SyncPerson(name: async.String, age: async.Int)
case class Person(future: Future[SyncPerson]) extends async.Any[SyncPerson, Person] {
def name = future.map(_.name).flatten
def age = future.map(_.age).flatten
}
As you can see, this approach is bloated, cumbersome and error-prone. And it's just the non-compiling and simplified version!
The solution is the @Async
macro annotation that FTypes provides. This macro does all that for you in a transparent way:
@Async case class Person(name: async.String, age: async.Int)
NOTE: as in the case of Array, async case classes can only hold async fields
Eventually (probably in the extremes of your code) you will need to operate with a more standard Scala way. FTypes provides an easy way to convert to (and from) Futures (Futures to the underlaying synchronous type):
val fi: Future[Int] = ???
val i = async.Int(fi)
val b: async.Boolean = ???
val fb: Future[Boolean] = b.future
The project is provided for:
- JVM
- ScalaJS
And Scala Versions:
- 2.10
- 2.11
- Improve this documentation (including the SBT section)
- Create async versions of StringOps, RichInt, ...
- Create an async version of the collections library
- Create the Option and Try monads
This project don't contain much code by itself. It's instead a composition of the following projects: