From nw
Guides functional programming in Scala 3 using ZIO, Cats Effect, opaque types for domain modeling, monadic chaining, error accumulation, and hexagonal architecture on JVM.
npx claudepluginhub nwave-ai/nwave --plugin nwThis skill uses the workspace's default tool permissions.
Cross-references: [fp-principles](./fp-principles.md) | [fp-domain-modeling](./fp-domain-modeling.md) | [pbt-jvm](./pbt-jvm.md)
Provides enterprise Scala expertise in functional programming (ZIO, Cats Effect), distributed systems (Pekko, Akka, Spark), reactive streams, and DDD/microservices patterns.
Provides enterprise Scala expertise in functional programming (ZIO/Cats Effect), distributed systems (Pekko/Akka/Spark), reactive architectures for system design, performance optimization, integration.
Applies Scala functional patterns including immutability, higher-order functions, pattern matching, ADTs, monads, for-comprehensions, and composition for type-safe applications.
Share bugs, ideas, or general feedback.
Cross-references: fp-principles | fp-domain-modeling | pbt-jvm
cs install scala3-compiler scala3-repl sbt
sbt new scala/scala3.g8 && cd order-service
# Add zio, zio-test, scalacheck to build.sbt
sbt compile && sbt test
enum PaymentMethod:
case CreditCard(cardNumber: String, expiryDate: String)
case BankTransfer(accountNumber: String)
case Cash
case class Customer(
customerId: CustomerId,
customerName: CustomerName,
customerEmail: EmailAddress
)
Case classes provide structural equality, copy, and pattern matching for free.
object OrderDomain:
opaque type OrderId = Int
object OrderId:
def apply(value: Int): OrderId = value
extension (id: OrderId) def value: Int = id
opaque type EmailAddress = String
object EmailAddress:
def from(raw: String): Either[ValidationError, EmailAddress] =
if raw.contains("@") then Right(raw)
else Left(InvalidEmail(raw))
Inside defining scope, alias is transparent. Outside, only exported operations available.
def placeOrder(raw: RawOrder): Either[OrderError, Confirmation] =
for
validated <- validateOrder(raw)
priced <- priceOrder(validated)
confirmed <- confirmOrder(priced)
yield confirmed
import cats.data.Validated
import cats.syntax.all.*
def validateCustomer(raw: RawCustomer): ValidatedNel[ValidationError, Customer] =
(validateName(raw.name), validateEmail(raw.email), validateAddress(raw.address))
.mapN(Customer.apply)
ZIO: ZIO[R, E, A] with built-in typed errors, DI (ZLayer), batteries-included. Cats Effect: IO[A], minimal type-class-based, Typelevel ecosystem (http4s, FS2, Doobie). Pick one and stay consistent.
trait OrderRepository:
def findOrder(id: OrderId): Task[Option[Order]]
def saveOrder(order: Order): Task[Unit]
def placeOrder(raw: RawOrder): ZIO[OrderRepository & PricingService, OrderError, Confirmation] =
for
repo <- ZIO.service[OrderRepository]
validated <- ZIO.fromEither(validateOrder(raw))
priced <- ZIO.fromEither(priceOrder(validated))
_ <- repo.saveOrder(priced)
yield Confirmation(priced.orderId)
// Adapter
class PostgresOrderRepository(ds: DataSource) extends OrderRepository:
def findOrder(id: OrderId): Task[Option[Order]] = ZIO.attemptBlocking { /* query */ }
def saveOrder(order: Order): Task[Unit] = ZIO.attemptBlocking { /* insert */ }
val appLayer: ZLayer[Any, Nothing, OrderRepository & PricingService] =
PostgresOrderRepository.layer ++ PricingServiceLive.layer
trait OrderRepository[F[_]]:
def findOrder(id: OrderId): F[Option[Order]]
def placeOrder[F[_]: Monad](repo: OrderRepository[F])(raw: RawOrder): F[Either[OrderError, Confirmation]] =
for
validated <- Monad[F].pure(validateOrder(raw))
result <- validated match
case Left(err) => Monad[F].pure(Left(err))
case Right(v) => repo.findOrder(v.orderId).map(_.toRight(OrderNotFound))
yield result
Frameworks: ScalaCheck (PBT) | ZIO Test (integrated PBT + unit) | ScalaTest (BDD) | MUnit (lightweight). See pbt-jvm for detailed PBT patterns.
import org.scalacheck.Properties
import org.scalacheck.Prop.forAll
object OrderSpec extends Properties("Order"):
property("serialization round-trips") = forAll { (order: Order) =>
deserialize(serialize(order)) == Right(order)
}
property("validated orders have positive totals") = forAll { (raw: RawOrder) =>
validateOrder(raw) match
case Left(_) => true
case Right(valid) => valid.total.value > 0
}
enum OrderState:
case Unvalidated(raw: RawOrder)
case Validated(order: ValidatedOrder)
case Priced(order: PricedOrder)
case Confirmed(confirmation: Confirmation)
def transition(state: OrderState, command: OrderCommand): Either[OrderError, OrderState] =
(state, command) match
case (OrderState.Unvalidated(raw), OrderCommand.Validate) =>
validateOrder(raw).map(OrderState.Validated(_))
case (OrderState.Validated(order), OrderCommand.Price) =>
priceOrder(order).map(OrderState.Priced(_))
case _ => Left(InvalidTransition(state, command))
extension (order: PricedOrder)
def totalWithTax(taxRate: BigDecimal): Money = Money(order.total.value * (1 + taxRate))
def isHighValue: Boolean = order.total.value > 1000
given instances close to their types. Deep resolution chains produce cryptic errors.