Setter
A Setter is a generalization of fmap from Functor.
Setter is write-only optic, allowing us to map into a structure and change out the content, but it cannot get the content.
Everything you can do with a Functor, you can do with a Setter.
Constructing monomorphic Setter
Using companion object
Setter[S, A] is constructed using the Setter[S, A]#apply function.
For a given Setter_[S, A] it takes a function as argument, (A => A) => S => S, which is a mapping function A => A and a structure S and returns a new structure S.
object Setter {
def apply[S, A](f: (A => A) => S => S): Setter[S, A]
}
import proptics.Setter
val listSetter: Setter[List[Int], Int] = Setter[List[Int], Int](f => ls => ls.map(f))
listSetter.over(_ + 1)(List.range(1, 6))
// val res0: List[Int] = List(2, 3, 4, 5, 6)
Using fromFunctor method
import proptics.Setter
import cats.instances.list
val listSetter: Setter[List[Int], Int] = Setter.fromFunctor[List, Int]
listSetter.over(_ + 1)(List.range(1, 6))
// val res1: List[Int] = List(2, 3, 4, 5, 6)
Constructing polymorphic Setter
Using companion object
Setter_[S, T, A, B] is constructed using the Setter_[S, T, A, B]#apply function.
For a given Setter_[S, T, A, B] it takes a function as argument, (A => B) => S => T, which is a mapping function A => B and a structure S and returns a structure of T.
object Setter_ {
def apply[S, T, A, B](f: (A => B) => S => T): Setter_[S, T, A, B]
}
Consider a Person data structure
case class Person(id: String, name: String, yearOfBirth: Int)
We have a person instance of type Person, and we want to compare it to another instance using cats.Eq.
In order to do it we need to use the id property of Person. We can use a Setter_ in order to change Eq[String] to Eq[Person]
import cats.Eq
import cats.syntax.eq._
import proptics.Setter_
final case class Person(id: String, name: String, yearOfBirth: Int)
val personEqSetter: Setter_[Eq[String], Eq[Person], Person, String] =
Setter_[Eq[String], Eq[Person], Person, String]((f: Person => String) => (eqString: Eq[String]) => {
Eq.instance[Person]((p1, p2) => eqString.eqv(f(p1), f(p2)))
})
val eqString: Eq[String] = Eq.fromUniversalEquals[String]
implicit val personEq: Eq[Person] = personEqSetter.over(_.id)(eqString)
Person("123", "Samuel Eilenberg", 1913) === Person("123", "Samuel Eilenberg", 1913)
// val res0: Boolean = True
We can also use the contramap method of cats.Eq in order to make the code more concise
import cats.Eq
import cats.syntax.eq._
import proptics.Setter_
import cats.syntax.contravariant._
final case class Person(id: String, name: String, yearOfBirth: Int)
val personEqSetter: Setter_[Eq[String], Eq[Person], Person, String] =
Setter_[Eq[String], Eq[Person], Person, String]((f: Person => String) => (eqString: Eq[String]) => {
eqString.contramap[Person](f)
})
val eqString: Eq[String] = Eq.fromUniversalEquals[String]
implicit val personEq: Eq[Person] = personEqSetter.over(_.id)(eqString)
Person("123", "Samuel Eilenberg", 1913) === Person("123", "Samuel Eilenberg", 1913)
// val res1: Boolean = True
Using fromContravariant method
import cats.Eq
import cats.syntax.eq._
import proptics.Setter_
final case class Person(id: String, name: String, yearOfBirth: Int)
val personEqSetter: Setter_[Eq[String], Eq[Person], Person, String] =
Setter_.fromContravariant[Eq, Person, String]
implicit val eqString: Eq[String] = Eq.fromUniversalEquals[String]
implicit val personEq: Eq[Person] = personEqSetter.over(_.id)(eqString)
Person("123", "Samuel Eilenberg", 1913) === Person("123", "Samuel Eilenberg", 1913)
// val res2: Boolean = True
Methods
set
/** set the modified focus of a Setter */
def set(a: A): S => S
import proptics.Setter
val listSetter: Setter[List[Int], Int] = Setter[List[Int], Int](f => ls => ls.map(f))
listSetter.set(9)(List.range(1, 6))
// res0: List[Int] = List(9, 9, 9, 9, 9)
over
/** modify the focus of a Setter using a function */
def over(f: A => A): S => S
import proptics.Setter
val listSetter: Setter[List[Int], Int] = Setter[List[Int], Int](f => ls => ls.map(f))
listSetter.over(_ + 1)(List.range(1, 6))
// res1: List[Int] = List(2, 3, 4, 5, 6)
Setter internal encoding
Polymorphic Setter
Setter_[S, T, A, B]
Setter_[S, T, A, B] is a function (A => B) => S => T.
/**
* @tparam S the source of a Setter_
* @tparam T the modified source of a Setter_
* @tparam A the focus of a Setter_
* @tparam B the modified focus of a Setter_
*/
abstract class Setter_[S, T, A, B] {
def apply(pab: A => B): S => T
}
Setter_[S, T, A, B] changes its focus from A to B, resulting in a change of structure from S to T.
A Setter that changes its focus/structure, is called Polymorphic Setter.
Monomorphic Setter
Setter[S, A]
Setter[S, A] is a type alias for Setter_[S, S, A, A], which has the same type of focus A, thus preserving the same type of structure S.
type Setter[S, A] = Setter_[S, S, A, A]
A Setter that does not change its focus/structure, is called Monomorphic Setter.
Laws
A Setter must satisfy all SetterLaws. These laws reside in the proptics.law package.
import cats.instances.list._
import cats.syntax.eq._
import cats.Eq
import proptics.Setter
val fromFunctor: Setter[List[Int], Int] = Setter.fromFunctor[List, Int]
Mapping with identity function will get you the same value
def overIdentity[S: Eq, A](setter: Setter[S, A], s: S): Boolean =
setter.over(identity)(s) === s
overIdentity(fromFunctor, List(1, 2, 3, 4))
// val res0: Boolean = true
Running twice with different handlers is equivalent to running it once with the composition of those handlers
def composeOver[S: Eq, A](setter: Setter[S, A], s: S)(f: A => A)(g: A => A): Boolean =
setter.over(g)(setter.over(f)(s)) === setter.over(g compose f)(s)
composeOver(fromFunctor, List(1, 2, 3, 4))(_ + 1)(_ * 2)
// val res1: Boolean = true
Setting twice is the same as setting once
def setSet[S: Eq, A](setter: Setter[S, A], s: S, a: A): Boolean =
setter.set(a)(setter.set(a)(s)) === setter.set(a)(s)
setSet(fromFunctor, List(1, 2, 3, 4), 9)
// val res2: Boolean = true