Alessandro Lacava

on Designing and Developing Software. In love with Functional Programming.

Keep Your Code Clean With Algebraic Data Types (ADTs)

Recently, Daniel Westheide wrote an interesting post about the abuse of the Option type in Scala. You can find it here. I couldn’t agree more with Daniel.

This short story is another example that demonstrates how using Option is not always the best option (pun intended).

I’m developing an advertising service for a customer using Scala. A simplified version of the Ad data structure is the following:

1
2
3
4
5
final case class Ad(
    headline: String,
    description1: String,
    description2: String
)

At some point they told me we need to support, by adding the headline2 field, two types of ad: standard and expanded. They said: “If headline, description1, and description2 are used, it is a standard ad. If headline, headline2, and description1 are used it is an expanded one. Users won’t include headline2 when the ad is intended to be standard, and won’t include description2 when the ad is intended to be expanded.”

The optionality of the headline2 and description2 fields could put you in the wrong direction. Indeed, you could think of changing the data structure as follows:

1
2
3
4
5
6
final case class Ad(
    headline: String,
    headline2: Option[String],
    description1: String,
    description2: Option[String]
)

This may sound fine but you could reach a much cleaner and type-safe solution using a simple Algebraic Data Type (ADT). Here’s how:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sealed trait Ad

object Ad {
  final case class Standard(
      headline: String,
      description1: String,
      description2: String
  ) extends Ad

  final case class Expanded(
      headline: String,
      headline2: String,
      description1: String
  ) extends Ad
}

This way you don’t need to inspect the headline2 and/or description2 value to find out the ad type you’re dealing with. Indeed, the ad type is encoded in the type system which is always a desiderata.

Bottom line: use Option with caution.

Scala: Seq, Map and Set as Functions

Yesterday my mate asked me: “I have a List[String] and a Map[String, Int] and I want a List[Int] where its values are those of the Map whose keys match the List[String] elements, maintaining the order. Should I use pattern matching?”. I know, the sentence is a bit convoluted but the code will make it clear, hopefully. Anyway, I replied: “No, you don’t need pattern matching, you just need this”:

1
2
3
4
5
6
7
8
scala> val m = Map("a" -> 1, "b" -> 2, "c" -> 3)
m: scala.collection.immutable.Map[String,Int] = Map(a -> 1, b -> 2, c -> 3)

scala> val l = List("a", "c", "b")
l: List[String] = List(a, c, b)

scala> l collect m
res0: List[Int] = List(1, 3, 2)

Hold on, how does it work? If you look at the definition of the collect method you’ll see it accepts a PartialFunction, instead I passed a Map to it. Well, it turns out that Map is a PartialFunction.

Since this peculiarity surprised him I decided to write a small post showing how Scala’s Map, List (actually Seq) and Set can be viewed as functions.

Before starting: functions vs partial functions

In short, a function is a mapping A => B that relates each value of type A to a value of type B–modulo bottom. A and B are called domain and codomain, respectively. If you’re not a math addict, roughly speaking, the domain is the set of all values that you may provide as input to your function, while the codomain is the result of the function application to the input, that is your function output.
On the other hand a partial function from A to B is not defined for some inputs of type A. E.g.:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// function
scala> val abs: Double => Double = x => if (x > 0) x else -x
abs: Double => Double = <function1>

scala> abs(42)
res1: Double = 42.0

scala> abs(-42)
res2: Double = 42.0

// partial function
scala> val sqrt: PartialFunction[Double, Double] = {
     |   case x if x >= 0 => math.sqrt(x)
     | }

scala> sqrt(4)
res3: Double = 2.0

scala> sqrt(-1)
scala.MatchError: -1.0 (of class java.lang.Double)
  at scala.PartialFunction$$anon$1.apply(PartialFunction.scala:253)
  at scala.PartialFunction$$anon$1.apply(PartialFunction.scala:251)
  at $anonfun$1.applyOrElse(<console>:7)
  at $anonfun$1.applyOrElse(<console>:7)
  at scala.runtime.AbstractPartialFunction$mcDD$sp.apply$mcDD$sp(AbstractPartialFunction.scala:36)
  ... 33 elided

Note that the PartialFunction definition is the following:

1
trait PartialFunction[-A, +B] extends (A) => B

That is a PartialFunction is a Function that will just throw for those inputs the partial function is not defined at. So you can use a PartialFunction wherever a Function is expected. Just keep in mind you’ll get an exception for some input values.

Seq[A] as PartialFunction[Int, A]

Being List an indirect subclass of collection.Seq and given that the latter has the following definition, you can see clearly that every Seq[A] is also a PartialFunction[Int, A]:

1
trait Seq[+A] extends PartialFunction[Int, A] with ...

Here’s an example:

1
2
3
4
5
6
7
8
scala> val xs = List("a", "c", "b")
l: List[String] = List(a, c, b)

scala> val f1: PartialFunction[Int, String] = xs
f1: PartialFunction[Int,String] = List(a, c, b)

scala> f1(0)
res4: String = a

Of course I could have used xs directly without the assignment to f1:

1
2
scala> xs(0)
res2: String = a

I assigned the list to f1 just to emphasise the fact that it’s a partial function. It corresponds to the index-based lookup.

Performance concern: Take into account that the index-based lookup on List has a cost of O(n). For this type of access you may consider using a Vector which has constant-time access cost. Anyway this post is not about performance concerns about the collection API so I won’t dig into this topic.

Map[A, B] as PartialFunction[A, B]

If you look at the Map definition you’ll see that it extends MapLike which, in turn, extends PartialFunction. So you can use it as follows:

1
2
3
4
5
6
7
8
scala> val m = Map("a" -> 1, "b" -> 2, "c" -> 3, "d" -> 4)
m: scala.collection.immutable.Map[String,Int] = Map(a -> 1, b -> 2, c -> 3, d -> 4)

scala> val f2: PartialFunction[String, Int] = m
f2: PartialFunction[String,Int] = Map(a -> 1, b -> 2, c -> 3, d -> 4)

scala> f2("a")
res5: Int = 1

Set[A] as A => Boolean

Here’s the definition of Set:

1
trait Set[A] extends (A) => Boolean with ...

It, evidently, extends A => Boolean which, as you probably already know, is just syntactic sugar for the more verbose Function[A, Boolean]. Example:

1
2
3
4
5
6
7
8
scala> val s = Set("a", "b", "c")
s: scala.collection.immutable.Set[String] = Set(a, b, c)

scala> val f3: String => Boolean = s
f3: String => Boolean = Set(a, b, c)

scala> f3("a")
res6: Boolean = true

So, for instance, you can use a set to filter a list:

1
2
3
4
5
6
7
8
scala> val xs = List("a", "c", "b")
xs: List[String] = List(a, c, b)

scala> val s = Set("a", "b", "d")
s: scala.collection.immutable.Set[String] = Set(a, b, d)

scala> xs filter s
res7: List[String] = List(a, b)

Conclusions

As a final consideration take into account that Seqs and Maps are partial functions while Set is a function. Partial functions could introduce insidious bugs. For instance, consider the very first example of this post. If the Map hadn’t contained all the elements of the List and I had used the map method instead of collect I would have introduced a bug:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
scala> val xs = List("a", "b", "c", "d")
xs: List[String] = List(a, b, c, d)

scala> val m = Map("a" -> 1)
m: scala.collection.immutable.Map[String,Int] = Map(a -> 1)

scala> xs map m
java.util.NoSuchElementException: key not found: b
  at scala.collection.MapLike$class.default(MapLike.scala:228)
  at scala.collection.AbstractMap.default(Map.scala:59)
  at scala.collection.MapLike$class.apply(MapLike.scala:141)
  at scala.collection.AbstractMap.apply(Map.scala:59)
  at scala.collection.immutable.List.map(List.scala:277)
  ... 33 elided

This is because map accepts a function and providing a partial function instead you get the exception for not valid inputs as I said in the functions vs partial functions section.

From now on, whenever you have a collection hanging around, consider looking at it as a function. This could help to solve your problem without using pattern matching or other boilerplate.

Scala Case Classes In Depth

For this post I’ll consider the following simple case class unless otherwise specified:

1
case class Person(lastname: String, firstname: String, birthYear: Int)

Common knowledge about case classes

When you declare a case class the Scala compiler does the following for you:

  • Creates a class and its companion object.
  • Implements the apply method that you can use as a factory. This lets you create instances of the class without the new keyword. E.g.:
1
2
3
4
val p = Person("Lacava", "Alessandro", 1976)

// instead if the slightly more verbose:
val p = new Person("Lacava", "Alessandro", 1976)
  • Prefixes all arguments, in the parameter list, with val. This means the class is immutable, hence you get the accessors but no mutators. E.g.:
1
2
3
val lastname = p.lastname
// the following won't compile:
p.lastname = "Brown"
  • Adds natural implementations of hashCode, equals and toString. Since == in Scala always delegates to equals, this means that case class instances are always compared structurally. E.g.:
1
2
3
4
5
val p_1 = Person("Brown", "John", 1969)
val p_2 = Person("Lacava", "Alessandro", 1976)

p == p_1 // false
p == p_2 // true
  • Generates a copy method to your class to create other instances starting from another one and keeping some arguments the same. E.g.: Create another instance keeping the lastname and changing firstname and birthYear:
1
2
// the result is: Person(Lacava,Michele,1972), my brother :)
val p_3 = p.copy(firstname = "Michele", birthYear = 1972)
  • Probably, most importantly, since the compiler implements the unapply method, a case class supports pattern matching. This is especially important when you define an Algebraic Data Type (ADT). E.g.:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
sealed trait Maybe[+T]
case class Value[T](value: T) extends Maybe[T]
case object NoValue extends Maybe[Nothing]

val v: Maybe[Int] = Value(42)
val v_1: Maybe[Int] = NoValue

def logValue[T](value: Maybe[T]): Unit = value match {
  case Value(v) => println(s"We have a value here: $v")
  case NoValue => println("I'm sorry, no value")
}

logValue(v) // prints We have a value here: 42
logValue(v_1) // prints I'm sorry, no value

As you probably already know, when your class has no argument you use a case object instead of a case class with an empty parameter list.

Apart from being used in pattern matching the unapply method lets you deconstruct a case class to extract it’s fields, both during pattern matching and as a simple expression to extract some of its fields. E.g.:

1
2
3
val Person(lastname, _, _) = p

println(lastname) // prints Lacava

Not so common knowledge about case classes

  • What if you need a function that, given your case class arguments as parameters, creates an instance of the class? Here’s how you can do it by partially applying apply (no pun intended :)):
1
2
3
4
val personCreator: (String, String, Int) => Person = Person.apply _

// the result is: Person(Brown,John,1969)
personCreator("Brown", "John", 1969)
  • What if you want your function, from the previous point, to be curried? Enters the curried method:
1
2
3
4
5
6
val curriedPerson: String => String => Int => Person = Person.curried

val lacavaBuilder: String => Int => Person = curriedPerson("Lacava")

val me = lacavaBuilder("Alessandro")(1976)
val myBrother = lacavaBuilder("Michele")(1972)
  • What about obtaining a function that accepts a tuple whose arity is equal to the number of the case class arguments, and produces an instance of the class? Well, there’s the tupled method for that:
1
2
3
4
5
val tupledPerson: ((String, String, Int)) => Person = Person.tupled

val meAsTuple: (String, String, Int) = ("Lacava", "Alessandro", 1976)

val meAsPersonAgain: Person = tupledPerson(meAsTuple)
  • You could also need a function that, given an instance of your class as input, produces an Option[TupleN[A1, A2, ..., AN]] as output, where N is the number of the case class arguments and A1, A2, ..., AN are their types. E.g.:
1
2
3
val toOptionOfTuple: Person => Option[(String, String, Int)] = Person.unapply _

val x: Option[(String, String, Int)] = toOptionOfTuple(p) // Some((Lacava,Alessandro,1976))

The curried and tupled methods are inherited from AbstractFunctionN which is extended by the autogenerated companion object. N is the number of the case class formal parameters. Note that, of course, if N = 1 you won’t get curried and tupled because they wouldn’t make sense for just one parameter!

Defining a case class using the curried form

There’s another less-known way of defining a case class, e.g.:

1
case class Keyword(text: String)(source: String, foo: Int)

The formal parameters in the first parameter section of a case class (just text in this case) are called elements; they are treated specially. All the goodies you get when you define a case class (accessors, pattern matching support, copy method, …) only apply to the first section. For example you don’t have an accessor for source since the compiler didn’t implicitly prefix it with val, like it did for text instead. E.g.:

1
2
3
4
val k1 = Keyword("restaurant")("storage", 1)

// won't compile
val source = k1.source

You can solve the accessor problem by prefixing the parameters with val. E.g.:

1
case class Keyword(text: String)(val source: String, val foo: Int)

Anyway you still won’t get all the other case class features. For instance, you cannot use the copy method by specifying only the source parameter. You have to specify, at least, all the parameters of the sections successive to the first. E.g.:

1
2
3
4
5
// won't compile
val k2 = k1.copy()(source = "web")

// will compile
val k3 = k1.copy()(source = "web", foo = 1)

Finally, the companion object of a case class defined in such a way won’t extend AbstractFunctionN, so the tupled and curried methods are not available.

At this point the natural question that may arise is: “Why on earth should I want to define a case class in such a way?” Apparently there are cases when it could be a reasonable choice. For example suppose that, for your business model, two instances of Keyword are to be considered equal iff they have the same text field. Well, in such a case by defining the case class using the curried form you’ll get what you want. E.g.:

1
2
3
4
val k1 = Keyword("restaurant")("storage", 1)
val k2 = Keyword("restaurant")("web", 2)

k1 == k2 // true!

That’s because also the equals implementation, you get for free for case classes, only applies to the first parameter section, so only to text in this case. I’m not saying here that this is always the best choice but it could be of help in certain situations.

In fact, you could define your case class as usual and override equals on your own. However overriding equals is not very trivial. Indeed, before doing that I recommend you read the chapter 30 of Programming in Scala: A Comprehensive Step-by-Step Guide, 2nd Edition - Odersky, Spoon, Venners. Its title is Object Equality and it’s just 25 pages long!

For the most curious ones

Furthermore, since each case class extends the Product trait it inherits the following methods:

  • def productArity: Int, returns the size of this product. In this case it corresponds to the number of arguments in the case class. E.g.:
1
2
3
val p = Person("Lacava", "Alessandro", 1976)

val arity = p.productArity // equals to 3
  • def productElement(n: Int): Any, returns the n-th element of this product, 0-based. In this case it corresponds to the n-th argument of the class. E.g.:
1
val lastname: Any = p.productElement(0) // Lacava
  • def productIterator: Iterator[Any], returns an iterator over all the elements of this product which, in the case class context, they are its arguments.

  • def productPrefix: String, returns a string used in the toString method of the derived classes. In this case it’s the name of the class. E.g.:

1
val className: String = p.productPrefix // the result is Person

Final Notes

  • I used type declarations in many expressions just to make things clearer. Of course I could have left them out and let the type inferer do its job.

  • Some Product’s methods return Any-based types, namely productElement and productIterator. For example, p.productElement(0) returns the lastname but it is of type Any so if you need to use it as String you have to cast it, which is an operation you should strive to avoid as much as possible.

  • Product extends Equals so every case class also inherits the canEqual method but, of course, going into its details is not the scope of this post. Besides, you don’t have to worry about it because it’s used interally by the autogenerated equals method, unless you decide to implement your own version of equals in which case you need to take into account canEqual. Again, in such a case I strongly suggest you read the chapter cited here.

Scala’s Self-recursive Types

One of the advantages of using a statically typed language is that you can use the type system to enforce some constraints. Scala provides self-recursive types, aka F-bounded polymorphic types that–along with self types–let you put powerful constraint to your type definitions.

Self-recursive type definition

Terminology apart, let me show you one of the use cases where this could be useful. Consider the following example which does not use a self-recursive type:

1
2
3
trait Doubler[T] {
  def double: T
}
1
2
3
case class Square(base: Double) extends Doubler[Square] {
  override def double: Square = Square(base * 2)
}

So far so good, the compiler will not complain. The problem is that it won’t complain even if you write something outrageous like the following code:

1
2
3
4
5
case class Person(firstname: String, lastname: String, age: Int)

case class Square(base: Double) extends Doubler[Person] {
  override def double: Person = Person("John", "Smith", 42)
}

You want to avoid something like that by enforcing a compile-time check. Enters a self-recursive type:

1
2
3
trait Doubler[T <: Doubler[T]] {
  def double: T
}

By using this definition of Doubler you’re saying: “Hey, if someone tries to extends Doubler with a type which doesn’t extend Doubler in turn (hence self-recursive), do not compile it”. In this case the previous definition of Square, which extends Doubler[Person], doesn’t compile.

Note that self-recursive types are not specific to Scala. Indeed Java uses them too. Take, for example, the Enum definition:

1
2
3
public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {
...
}

E extends Enum<E> in Javanese means exactly E <: Enum[E]

Self type definition

F-bounded polymorphic types are of great help but sometimes they are not enough to enforce the constraints you need. Indeed, the previous definition of Doubler has still one problem. Consider the next code:

1
2
3
4
5
6
7
8
9
10
11
trait Doubler[T <: Doubler[T]] {
  def double: T
}

case class Square(base: Double) extends Doubler[Square] {
  override def double: Square = Square(base * 2)
}

case class Apple(kind: String) extends Doubler[Square] {
  override def double: Square = Square(5)
}

Can you spot the problem? Look at the Apple definition, it extends Doubler[Square] instead of Doubler[Apple].

This code compiles because it respects the constraint put by the Doubler definition. Indeed Square extends Doubler so it can be used in Apple. Sometimes this is what you want in which case the self-recursive type will do. In cases when you don’t want this to happen a self type can work this out:

1
2
3
trait Doubler[T <: Doubler[T]] { self: T =>
  def double: T
}

Now if you try to compile the previous definition of Apple the compiler will complain by saying something like:

1
2
3
4
error: illegal inheritance;
 self-type Apple does not conform to Doubler[Square]'s selftype Square
       case class Apple(kind: String) extends Doubler[Square] {
                                              ^

Conclusions

If you’re thinking: “Come on! I would never extend Apple that way because I know what I meant when I wrote my Doubler abstraction. I don’t need then the self type annotation and, since I know what I’m doing, I don’t need the self-recursive type either”. Well you may be right but I’d have two objections:

  1. Generally you are not the only one working on a project and, anyway, a good rule of thumb is to design your software as if you’re designing a public API. In this case you want to be sure no one will use your API in the wrong way.

  2. Compilers are implemented by smart guys, generally. Having the compiler help by your side is always a good thing in my humble opinion.

Are there alternatives to this type of problems? Yes indeed, Type Classes, which is by the way the option I prefer. But this is another story for a future post.

Scala DSL for Currency-related Operations

A simple internal DSL in Scala for money-related operations

Source code: Scala DSL for money-related operations

This Domain-Specific Language (DSL) lets you perform operations among different currencies, by transparently doing all internal conversions. The conversion map is injected implicitly by the client code.

Usage Example

Here’s a simple usage example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import com.lambdista.money._
import com.lambdista.money.Currency._

object Main {

  def main(args: Array[String]): Unit = {

    val conversion: Conversion = Map(
      (GBP, EUR) -> 1.270,
      (EUR, USD) -> 1.268,
      (GBP, USD) -> 1.611
    )

    implicit val converter = Converter(conversion)

    val sumAndConversion1 = 100.001(USD) + 200(EUR) to GBP
    println(s"sumAndConversion1: $sumAndConversion1")

    val sumAndConversion2: Money = 100(USD) + 210.4(EUR) to EUR
    println(s"sumAndConversion2: $sumAndConversion2")

    val sum = 100.001(USD) + 200(EUR)
    val simpleConversion = sum(GBP)
    println(s"simpleConversion: $simpleConversion")

    val sumWithSimpleNumber = 100(USD) + 23.560
    println(s"sumWithSimpleNumber: $sumWithSimpleNumber")

    val multiplicationWithSimpleNumber = 100(USD) * 23
    println(s"multiplicationWithSimpleNumber: $multiplicationWithSimpleNumber")

    val usd = Currency("USD")

    val multiplication = 100(usd) * 23(EUR)
    println(s"multiplication: $multiplication")

    val divisionWithSimpleNumber = 100(USD) / 23
    println(s"divisionWithSimpleNumber: $divisionWithSimpleNumber")

    val comparison = 100(USD) > 90(EUR)
    println(s"100 USD > 90 EUR? $comparison")

  }

}

As you can see the client code just needs two simple imports and an implicit value of type Converter in order to use the DSL. The operations shown in the previous code are only a few among the available ones. Have a look at the Money class for a complete coverage.

You can find the source code here: Scala DSL for money-related operations

Try-Success-Failure API for Java 8

Try-Success-Failure API: Java implementation of the famous Scala counterpart

Source code: Try-Success-Failure API for Java

This API is a Java implementation of Scala Try API, originally implemented by the guys at Twitter and later added to the Scala Standard Library.

The Try type represents a computation that may fail. If the computation is successful returns the value wrapped in a Try.Success otherwise returns the java.lang.Exception wrapped in a Try.Failure.

To use Try you need to call the Try.apply(FailableSupplier) method passing in a lambda with the same signature used for a common java.util.function.Supplier. Indeed FailableSupplier is just a java.util.function.Supplier with a throws Exception added to its get method.

For example, Try can be used to perform division on a user-defined input, without the need to do explicit exception-handling in all of the places that an exception might occur.

An important property of Try shown in the divide method of the MainExample class is its ability to pipeline (chain if you prefer) operations, catching exceptions along the way thanks to its flatMap method. If you are not a seasoned functional programming geek concepts such as flatMap/map might not be easy to grasp at first. However you’ll get used to them and, in the end, you’ll love them. Moreover you’re going to encounter these methods more and more often since some important Java 8 classes already implement them (e.g. java.util.Optional and java.util.stream.Stream. Anyway for the moment just take for granted that to pipeline more than two operations, say N, you just need to chain them by using N - 1 flatMap calls and a last call to map. E.g.: Suppose you have 3 variables (x, y and z) being of type Try and you just want to sum them up. The code you need for doing that is the following:

1
x.flatMap(a -> y.flatMap(b -> z.map(c -> a + b + c)))

Apart from map and flatMap, Try has many other useful methods. See the TryTest class for a thorough coverage of all methods.

Usage example

1
2
3
4
5
6
7
8
9
10
11
12
System.out.println("Integer division");
System.out.println("Enter the dividend press Return and then enter the divisor: ");
Scanner dividend = new Scanner(System.in);
Scanner divisor = new Scanner(System.in);

Try<Integer> num = Try.apply(dividend::nextInt);
Try<Integer> denom = Try.apply(divisor::nextInt);

Try<Integer> result = num.flatMap(x -> denom.map(y -> x / y));
Try<String> resultTryStr = result.map(i -> "The result of division is: " + i);
String resultStr = resultTryStr.getOrElse("You must've divided by zero or entered something that's not an Int. Try again!");
System.out.println(resultStr);

In the previous example if you enter two valid integers with the second one–the divisor–being different from zero then the code prints out The result of division is: $RESULT, where $RESULT is the division between the first and the second number. On the other hand, if you either enter non valid integers–such as a string–or the second number is zero then you’ll get the message You must've divided by zero or entered something that's not an Int. Try again! printed out.

Anyway, as I already said, see the TryTest class for a thorough coverage of all methods.

Final Notes

Any criticism/suggestion is more than welcome!

Source code: Try-Success-Failure API for Java

Combine JavaScript and a Template Engine for Flexible Web Apps

Combine the power and simplicity of the TrimPath template engine with JavaScript and Ajax to develop next-generation web applications.

Template engines (TEs) can be very useful in web development scenarios where you need to generate and format text automatically according to specific processing rules. These engines can also help you build your applications based on the Model-View-Controller (MVC) pattern, making them more robust and maintainable than applications based on spaghetti code. Most programming languages provide built-in or third-party TEs. Java, for example, has Velocity and FreeMarker, among others. For PHP, Smarty is the most used TE.

Go to developer.com to read the rest of the article: Combine JavaScript and a Template Engine for Flexible Web Apps.

Script.aculo.us Controls: Do Your Web Users a Favor

Learn how to integrate Script.aculo.us web controls into your web applications to make the end-user experience more pleasant.

In a previous DevX article, I wrote about the JavaScript framework Prototype, which aims to ease the development of dynamic web applications. That article ended with just a few words about Script.aculo.us, which is a fantastic UI library based on Prototype. This article gives Script.aculo.us its due by examining the web controls it provides: autocompleters, sliders, and in-place editors.

Script.aculo.us is a pretty big library, so no single article can cover it completely. However, the knowledge you will gain will enable you to leverage the power of Script.aculo.us web controls to improve the end-user experience.

Go to DevX to read the rest of the article: Script.aculo.us Controls: Do Your Web Users a Favor.