Coder Social home page Coder Social logo

zio / zio-prelude Goto Github PK

View Code? Open in Web Editor NEW
438.0 41.0 110.0 16.04 MB

A lightweight, distinctly Scala take on functional abstractions, with tight ZIO integration

Home Page: https://zio.dev/zio-prelude

License: Apache License 2.0

Scala 100.00%
zio scala functional-programming category-theory abstract-algebra

zio-prelude's Introduction

ZIO Prelude

ZIO Prelude is a lightweight, distinctly Scala takes on functional abstractions, with tight ZIO integration.

Production Ready CI Badge Sonatype Releases Sonatype Snapshots javadoc ZIO Prelude

Introduction

ZIO Prelude is a small library that brings common, useful algebraic abstractions and data types to scala developers. It is an alternative to libraries like Scalaz and Cats based on radical ideas that embrace modularity and subtyping in Scala and offer new levels of power and ergonomics. It throws out the classic functor hierarchy in favor of a modular algebraic approach that is smaller, easier to understand and teach, and more expressive.

ZIO Prelude has three key areas of focus:

  1. Data structures, and type classes for traversing them. ZIO Prelude embraces the collections in the Scala standard library, and extends them with new instances and new useful additions.
  2. Patterns of composition for types. ZIO Prelude provides a small catalog of patterns for binary operators, which combine two values into another value of the same type. These patterns are named after the algebraic laws they satisfy: associativity, commutativity, and identity.
  3. Patterns of composition for type constructors. ZIO Prelude provides a catalog of patterns for binary operators on type constructors (things like Future, Option, ZIO Task). These patterns are named after the algebraic laws they satisfy (associativity, commutativity, and identity) and the structure they produce, whether a tuple or an either.

Design principles behind ZIO Prelude:

  1. Radical — So basically it ignores all dogma, and it is completely written with a new mindset.
  2. Orthogonality — The goal for ZIO Prelude is to have no overlap. Type classes should do one thing and fit it well. So there is not any duplication to describe type classes.
  3. Principled — All type classes in ZIO Prelude include a set of laws that instances must obey.
  4. Pragmatic — If we have data types that don't satisfy laws but that are still useful to use in most cases, we can go ahead and provide instances for them.
  5. Scala-First - It embraces subtyping and benefit from object-oriented features of Scala.

ZIO Prelude gives us:

  • Functional Data Types— Additional data types to supplement the ones in the Scala standard library such as Validation and NonEmptyList to enable more accurate domain modeling and handle common problems like data validation. For example:
    • NonEmptyList, NonEmptySet
    • ZSet, ZNonEmptySet
    • Validation, ZValidation
  • Functional Abstractions— Functional abstractions to describe different ways of combining data, making it easy for us to combine complex data types in a principled way.
  • New Types— that allow to increase type safety in domain modeling. Wrapping existing type adding no runtime overhead. These refined newtypes allow us to increase the type safety of our code base with zero overhead and minimal boilerplate.
  • ZPure— A description of a computation that supports logging, context, state, and errors, providing all the functionality traditionally offered by monad transformers with dramatically better performance and ergonomics.

The library has a small research-stage package (zio.prelude.fx) that provides abstraction over expressive effect types like ZIO and ZPure.

ZIO Prelude is a library focused on providing a core set of functional data types and abstractions that can help you solve a variety of day to day problems. The tools provided by ZIO Prelude fall into the following main categories:

Installation

In order to use this library, we need to add the following line in our build.sbt file:

libraryDependencies += "dev.zio" %% "zio-prelude" % "1.0.0-RC23"

Example

In this example, we are going to create a simple voting application. We will use two features of ZIO Prelude:

  1. To become more type safety we are going to use New Types and introducing Topic and Votes data types.
  2. Providing instance of Associative type class for Votes data type which helps us to combine Votes values.
import zio.prelude._

object VotingExample extends scala.App {

  object Votes extends Subtype[Int] {
    implicit val associativeVotes: Associative[Votes] =
      new Associative[Votes] {
        override def combine(l: => Votes, r: => Votes): Votes =
          Votes(l + r)
      }
  }
  type Votes = Votes.Type

  object Topic extends Subtype[String]
  type Topic = Topic.Type

  final case class VoteState(map: Map[Topic, Votes]) { self =>
    def combine(that: VoteState): VoteState =
      VoteState(self.map combine that.map)
  }

  val zioHttp    = Topic("zio-http")
  val uziHttp    = Topic("uzi-http")
  val zioTlsHttp = Topic("zio-tls-http")

  val leftVotes  = VoteState(Map(zioHttp -> Votes(4), uziHttp -> Votes(2)))
  val rightVotes = VoteState(Map(zioHttp -> Votes(2), zioTlsHttp -> Votes(2)))

  println(leftVotes combine rightVotes)
  // Output: VoteState(Map(zio-http -> 6, uzi-http -> 2, zio-tls-http -> 2))
}

Documentation

Learn more on the ZIO Prelude homepage!

Contributing

For the general guidelines, see ZIO contributor's guide.

Code of Conduct

See the Code of Conduct

Support

Come chat with us on Badge-Discord.

License

License

zio-prelude's People

Contributors

adamgfraser avatar arnoldlacko avatar bmarinovic avatar felher avatar ghostdogpr avatar github-actions[bot] avatar guizmaii avatar gurghet avatar jaliss avatar jdegoes avatar jorge-vasquez-2301 avatar jorokr21 avatar kamalkang avatar khajavi avatar kitlangton avatar landlockedsurfer avatar lemastero avatar michaelt293 avatar mijicd avatar mschuwalow avatar phderome avatar rayanral avatar renovate[bot] avatar scala-steward avatar shankarshastri avatar sideeffffect avatar simy4 avatar softinio avatar vigoo avatar yisraelu 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  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  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  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  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  avatar  avatar

zio-prelude's Issues

Add constructors to NonEmptyList[A] companion object

For example:

def apply[A](a: A, as: A*): NonEmptyList[A] = ???

def fromCons[A](as: ::[A]): NonEmptyList[A] = ???

def fromIterable[A](a: A, as: Iterable[A]): NonEmptyList[A] = ???

def unfold[Z, A](start: Z)(project: Z => A)(iterate: Z => Option[Z]): NonEmptyList[A] = ???
...

Support more natural law syntax

Right now our laws are a mixture of booleans and TestResult. Use of booleans, such as a > b, will result in quite poor error messages. Ideally, every law failure produces a richly detailed error message, fully utilizing ZIO Test's facilities for reporting.

We fake this right now using <-> as an alternate operator, but this doesn't scale: what would we choose for >, >=, etc.

I think it might be useful to define a new type, like Arbitrary / Arb:

object Arb extends SubtypeF
type Arb[A] = Arb.Type[A]

implicit class ArbSyntax[A](value: Arb[A]) {
  def === (that: Arb[A])(implicit equal: Equal[A]): TestResult = ???
  def > (that: Arb[A])(implicit ord: Ord[A]): TestResult = ???
  ...
}

Then to define (redefine?) Law1, etc. classes such that they are passed Arb[A] instead of A.

Then laws can have this form:

(a1 < a2) && (a2 < a3) ==> (a1 < a3)
(a1 === a2) ==> (a2 === a1)

But they will generate detailed error messages.

/cc @adamgfraser

Don't Render Fully Qualified Name in Default Debug Renderer

Currently the default renderer for Debug renders the fully qualified name of each constructor. So for example a Validation instance would be rendered as zio.prelude.Validation.Failure(zio.Chunk("some string")) whereas the rendering given by toString would be Failure(Chunk("some string")). While valid and perhaps optimal for compiling the rendering back to Scala, this is not the best default as it can lead to cluttered output when the constructor is deeply nested and inconsistent behavior depending on whether the user calls debug.render and toString even for user created types.

We should change the default renderer to render to unqualified name and add an additional renderer to render the fully qualified name. We can then test that the default rendering is equivalent to toString for all standard types.

Add smart constructor support for newtyping mechanism

  sealed trait NewtypeSmart[A] {
    type Type

    def make(value: A): Validation[String, Type] = wrap(value)

    def unapply(value: Type): Some[A] = Some(unwrap(value))

    def wrap(value: A): Validation[String, Type] = wrapAll[Id](value)

    def unwrap(value: Type): A = unwrapAll[Id](value)

    def wrapAll[F[_]](value: F[A]): Validation[String, F[Type]]

    def unwrapAll[F[_]](value: F[Type]): F[A]
  }

Etc.

Smart constructor newtypes / subtypes could utilize ZIO Test Assertion, e.g.:

object Natural extends SubtypeSmart[Int](v => v.isGreaterThan(0) || v.equalTo(0))
type Natural = Natural.Type

This way failures have descriptive error messages without additional coding. Although possibly we need a mechanism to override these error messages in the event they are going to be shown to a user.

Perform renames of higher-kinded type classes

  • AssociativeF.Both => AssociativeBothF
  • AssociativeF.Either => AssociativeEitherF
  • CommutativeF.Both => CommutativeBothF
  • CommutativeF.Either => CommutativeEitherF
  • IdentityF.Both => IdentityBothF
  • IdentityF.Either => IdentityEitherF

Add lawless instances

During my work on #24, I removed Identity instances for double and float. The reason I did this is because both of them violate identity laws. However, having in mind how useful these 2 types are, it makes sense to provide instances for them, but don't include them in law checking and document the reason for that.

Resolve Inconsistency in Rendering of Algebraic Data Types

Right now have some consistency in the rendering of algebraic data types. For example:

// Either
Repr.VConstructor(List("scala"), "Left", List(e.debug))

//
Debug.Repr.VConstructor(List("zio", "prelude"), "Validation.Failure", List(es.debug))

The Either subtype doesn't even have "Either" anywhere (possibly because it is exported in the scala package object), whereas the Validation subtype not only has the parent type name but included it as part of the name of the type itself so it will always be rendered, which is inconsistent with the normal treatment of case classes with toString.

I can see three potential options:

  1. Parent types of algebraic data types should be included in the list of namespaces. If by default we render without the namespace then this would achieve the property that the default rendering is consistent with toString, which seems desirable but could be debated.
  2. Parent types are included as part of the name of the class itself. That would result in a rendering that is less ambiguous (e..g several ZIO types have a Failure subtype. It would be inconsistent with toString, but maybe that is okay. It could result in crowded output if types are subtypes of a large nested ADT hierarchy. To some extent it throws away information because we're just putting all this in the class name so the renderer doesn't have much ability to make decisions based on it.
  3. We create a new parameter in VConstructor, maybe it becomes:
final case class VConstructor(namespace: List[SString], parents: List[SString], name: SString, reprs: List[Repr])

This probably creates some ambiguity of what should be part of namespace versus parents (maybe a guideline is parents are things that are super types of the type being rendered) but probably gives the renderer the most flexibility. That then still leaves the question of whether the default renderer should render the parents or not or possibly only the first parent.

Make `Equal` more performant

Right now, only Equal instances created with make will utilize refEq. But increasingly, it's more common to create subtypes directly.

To ensure these are performant, we should make the ref check mandatory, which we can do by making the equal method final and requiring subtypes implement some other method.

For example:

trait Equal[-A] { self =>
  final def equal(l: A, r: A): Boolean = refEq(l, r) || checkEqual(l, r)

  protected def checkEqual(l: A, r: A): Boolean
}

In this way, there is no question that the method will have an efficient implementation.

Implement Debug.Repr#toString

Note that String values must be printed in an escaped way, surrounded by double quotes, such that they can be copy/pasted into the REPL and reconstruct such strings identically.

Use Laws

We can upgrade to a snapshot version of ZIO and use the new laws functionality. This will both make it nicer for us to test them and allow us to identity improvement opportunities since I expect we will be one the heaviest users of this functionality. I am working on this.

Sketch out microsite

The microsite should probably have:

  • An introductory guide, which would cover several pages
  • A list of type classes, and explanations
  • A list of data types, and explanations
  • A section on worked examples of using type classes and data types
  • A 'background' section covering:
    • Type Classes
    • Operations
    • Laws

Make `NonEmptyList` extend Seq

NonEmptyList[A] should extend Seq[A] and should override all relevant methods to return a NonEmptyList[A] when possible.

Relax Identity's superclass to be Closure

Currently, every Identity implies Associative. But there is no part of Identity's laws that require Associative, so the supertype can be changed to Closure to permit more instances.

Utilize by-name parameters for any case where laziness is important

For some type class methods, in order to compute an answer, the parameters are both needed.

Good examples are:

  • Equal (cannot tell if two things are equal without looking at both of them)
  • Hash (cannot hash something without looking at it)
  • Debug (although we could fix this by making Debug.Repr lazy)

But for other type class methods, there exist some instances that would not need to evaluate both parameters.

Good examples are:

  • Closure
  • Associative
  • Commutative
  • Identity (+ Left/Right)

These methods can benefit from by-name parameters, to reduce the amount of computation.

How Should We Support "Test" Equal Instances?

We're starting to add instances for more data types that don't have well defined Equal instances. For example, Ord isn't a data type so we can't just look at two Ord values and say whether they are equal. At the same time, we would like to be able to test laws for them to make sure our instances are well behaved.

One approach to do this would be to allow defining some "test" Equal instances. For example, given two Ord[A] values we could say they are equal "as far as we can tell" if they return the same ordering for a random sample of values. We could not know for certain the values are actually equal unless the domain was very small, but this could still probably provide a high degree of confidence in the correctness of instances.

This would probably require performing effects in determining the "to our best knowledge" equality, especially for effect types but would require defining Equal instances that are not "true equality" in some sense, at least within our own internal test suites.

Does this make sense or is there another way we should approach this?

Setup CircleCI

Currently, we aren't using any CI, which is fine while in prototyping phase, but the codebase grows quite quickly, and "pushes" the language more and more :).

CI configuration present in .config is compatible with ZIO's CircleCI setup, and it will become quite useful once repository is transferred. Until that happens, I propose writing a minimal configuration that would run on CircleCI's free tier and perform only tests and format checks.

ZIO Prelude does not work from `sbt console`

How to reproduce:
Start sbt console in ZIO Prelude project directory from the shell.
Try to e.g. instantiate a Sum type using zio.prelude.Sum(1).

Output:

$ sbt console
[info] Loading global plugins from /Users/manfred/.sbt/1.0/plugins
[info] Loading settings for project zio-prelude-build from plugins.sbt ...
[info] Loading project definition from /Users/manfred/src/github/zio-prelude/project
[warn] There may be incompatibilities among your library dependencies; run 'evicted' to see detailed eviction warnings.
[info] Compiling 1 Scala source to /Users/manfred/src/github/zio-prelude/project/target/scala-2.12/sbt-1.0/classes ...
[info] Loading settings for project root from build.sbt ...
[info] Set current project to zio-prelude (in build file:/Users/manfred/src/github/zio-prelude/)
[info] Compiling 19 Scala sources to /Users/manfred/src/github/zio-prelude/target/scala-2.12/classes ...
[info] Starting scala interpreter...
Welcome to Scala 2.12.10 (Java HotSpot(TM) 64-Bit Server VM, Java 11.0.3).
Type in expressions for evaluation. Or try :help.

scala> zio.prelude.Sum(1)
<console>:5: error: illegal cyclic inheritance involving package object prelude
         lazy val $result = res0
                                                 ^
<console>:9: error: illegal cyclic inheritance involving package object prelude
       ""  + "\u001B[1m\u001B[34mres0\u001B[0m: \u001B[1m\u001B[32mzio.prelude.Sum.Type[Int]\u001B[0m = " + _root_.scala.runtime.ScalaRunTime.replStringOf(res0, 1000)

I tried using the latest version of sbt (1.3.7 -> 1.3.8), but this did not help.

Issues with `Nothing` Instances

Seeing a couple of issues with the implicit instances for Nothing. First, instances of parameterized on Nothing such as Equal[Nothing aren't available in implicit scope. Even if I move everything other than Nothing to a lower priority scope I still get this error. I think there is some kind of logic that Nothing does not qualify as a more specific type.

trait Equal[-A]

object Equal {
  
  implicit val IntEqual: Equal[Int] =
    new Equal[Int] {}
  
  implicit val StringEqual: Equal[String] =
    new Equal[String] {}
  
  implicit val NothingEqual: Equal[Nothing] =
    new Equal[Nothing] {}
}

// diverging implicits
implicitly[Equal[Nothing]]

Also, even if I define an Equal[Nothing] instance locally to make it implicitly available derived instances aren't available unless defining them extremely explicitly:

trait Equal[-A]

object Equal {
  
  implicit def EitherEqual[A: Equal, B: Equal]: Equal[Either[A, B]] =
    new Equal[Either[A, B]] {}
  
  implicit val IntEqual: Equal[Int] =
    new Equal[Int] {}
}

implicit val NothingEqual: Equal[Nothing] =
  new Equal[Nothing] {}

implicitly[Equal[Nothing]]
implicitly[Equal[Either[Int, Nothing]]] // Does not compile
Equal.EitherEqual[Int, Nothing]

Commutative Typeclass Instance for Either

As mentioned in pull request #53 Adam suggested that I put the following Commutative implementation for Either up for discussion here:

implicit def EitherCommutative[E: Commutative, A: Commutative]: Commutative[Either[E, A]] = 
  new Commutative[Either[E, A]] { 
    def combine(l: Either[E, A], r: Either[E, A]): Either[E, A] = 
      (l, r) match { 
        case (Right(l), Right(r)) => Right(l <> r)
        case (Left(l), Right(_))  => Left(l)
        case (Right(_), Left(r))  => Left(r)
        case (Left(l), Left(r))   => Left(l <> r)
      } 
  }

The implementation short-circuits in case one operand is Left, else it combines using Commutative from either E or A.

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.