├── project ├── build.properties └── plugins.sbt ├── haskell └── derivation-examples │ ├── .gitignore │ ├── Setup.hs │ ├── app │ └── Main.hs │ ├── stack.yaml.lock │ ├── src │ └── Lib.hs │ ├── package.yaml │ ├── derivation-examples.cabal │ └── stack.yaml ├── mu └── src │ └── main │ └── scala │ └── mu │ ├── package.scala │ ├── ProtobufMarshaller.scala │ ├── service.scala │ ├── CallHandlers.scala │ └── GrpcServerSide.scala ├── src └── main │ └── scala │ ├── myapp │ ├── Foo.scala │ ├── Person.scala │ ├── Weather.scala │ ├── Employee.scala │ ├── Pet.scala │ ├── MyList.scala │ └── ExprF.scala │ ├── Main.scala │ └── mylibrary │ ├── Show.scala │ └── Functor.scala ├── mu-service ├── greeter.proto └── src │ └── main │ └── scala │ └── myapp │ ├── Server.scala │ └── Greeter.scala ├── rust └── derivation-examples │ ├── Cargo.toml │ ├── src │ ├── lib.rs │ └── main.rs │ └── Cargo.lock └── README.md /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.3.8 2 | -------------------------------------------------------------------------------- /haskell/derivation-examples/.gitignore: -------------------------------------------------------------------------------- 1 | .stack-work/ 2 | *~ -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.4.1") 2 | -------------------------------------------------------------------------------- /haskell/derivation-examples/Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /mu/src/main/scala/mu/package.scala: -------------------------------------------------------------------------------- 1 | package mu 2 | 3 | enum Encoding { 4 | case Protobuf 5 | case Avro 6 | } 7 | -------------------------------------------------------------------------------- /haskell/derivation-examples/app/Main.hs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Lib 4 | 5 | main :: IO () 6 | main = someFunc 7 | -------------------------------------------------------------------------------- /src/main/scala/myapp/Foo.scala: -------------------------------------------------------------------------------- 1 | package myapp 2 | 3 | import mylibrary.Functor 4 | 5 | case class Foo[A](x: A, y: String) derives Functor 6 | -------------------------------------------------------------------------------- /mu/src/main/scala/mu/ProtobufMarshaller.scala: -------------------------------------------------------------------------------- 1 | package mu 2 | 3 | trait ProtobufMarshaller[T] extends io.grpc.MethodDescriptor.Marshaller[T] 4 | 5 | -------------------------------------------------------------------------------- /mu/src/main/scala/mu/service.scala: -------------------------------------------------------------------------------- 1 | package mu 2 | 3 | import scala.annotation.StaticAnnotation 4 | 5 | class service(encoding: Encoding) extends StaticAnnotation 6 | -------------------------------------------------------------------------------- /src/main/scala/myapp/Person.scala: -------------------------------------------------------------------------------- 1 | package myapp 2 | 3 | import mylibrary.Show 4 | 5 | case class Person( 6 | name: String, 7 | height: Int 8 | ) derives Show 9 | -------------------------------------------------------------------------------- /src/main/scala/myapp/Weather.scala: -------------------------------------------------------------------------------- 1 | package myapp 2 | 3 | import mylibrary.Show 4 | 5 | enum Weather derives Show { 6 | case Sunny 7 | case Cloudy 8 | case Rainy 9 | } 10 | -------------------------------------------------------------------------------- /src/main/scala/myapp/Employee.scala: -------------------------------------------------------------------------------- 1 | package myapp 2 | 3 | import mylibrary.Show 4 | 5 | enum Employee derives Show { 6 | case Grunt(name: String, boss: Employee) 7 | case CEO 8 | } 9 | -------------------------------------------------------------------------------- /src/main/scala/myapp/Pet.scala: -------------------------------------------------------------------------------- 1 | package myapp 2 | 3 | import mylibrary.Show 4 | 5 | case class Pet( 6 | name: String, 7 | species: String, 8 | owner: Person 9 | ) derives Show 10 | -------------------------------------------------------------------------------- /src/main/scala/myapp/MyList.scala: -------------------------------------------------------------------------------- 1 | package myapp 2 | 3 | import mylibrary.Functor 4 | 5 | enum MyList[+A] derives Functor { 6 | case Cons(head: A, tail: MyList[A]) 7 | case Nil 8 | } 9 | 10 | -------------------------------------------------------------------------------- /src/main/scala/myapp/ExprF.scala: -------------------------------------------------------------------------------- 1 | package myapp 2 | 3 | import mylibrary.Functor 4 | 5 | enum ExprF[A] derives Functor { 6 | case Lit(x: Int) 7 | case Add(x: A, y: A) 8 | case Mult(x: A, y: A) 9 | } 10 | 11 | final case class Fix[F[_]](unFix: F[Fix[F]]) 12 | -------------------------------------------------------------------------------- /mu-service/greeter.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package myapp; 4 | 5 | message HelloRequest { 6 | string name = 1; 7 | } 8 | 9 | message HelloResponse { 10 | string name = 1; 11 | } 12 | 13 | service Greeter { 14 | rpc SayHello(HelloRequest) returns (HelloResponse); 15 | } 16 | -------------------------------------------------------------------------------- /rust/derivation-examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "derivation_examples" 3 | version = "0.1.0" 4 | authors = ["Chris Birchall "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | syn = { version = "1.0.22", features = ["derive"] } 11 | 12 | [lib] 13 | proc-macro = true 14 | -------------------------------------------------------------------------------- /haskell/derivation-examples/stack.yaml.lock: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by Stack. 2 | # You should not edit this file by hand. 3 | # For more information, please see the documentation at: 4 | # https://docs.haskellstack.org/en/stable/lock_files 5 | 6 | packages: [] 7 | snapshots: 8 | - completed: 9 | size: 496112 10 | url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/15/13.yaml 11 | sha256: 75a1a0f870e1876898b117b0e443f911b3fa2985bfabb53158c81ab5765beda5 12 | original: lts-15.13 13 | -------------------------------------------------------------------------------- /mu-service/src/main/scala/myapp/Server.scala: -------------------------------------------------------------------------------- 1 | package myapp 2 | 3 | import io.grpc._ 4 | 5 | @main def runServer(): Unit = { 6 | val service = new Greeter { 7 | def SayHello(req: HelloRequest): HelloResponse = 8 | HelloResponse(s"Hi, ${req.name}!") 9 | } 10 | val serverServiceDef = service.toServerServiceDefinition 11 | val server = ServerBuilder.forPort(8080) 12 | .addService(serverServiceDef) 13 | .build() 14 | .start() 15 | println("Server started on port 8080") 16 | server.awaitTermination() 17 | } 18 | -------------------------------------------------------------------------------- /mu/src/main/scala/mu/CallHandlers.scala: -------------------------------------------------------------------------------- 1 | package mu 2 | 3 | import io.grpc.ServerCallHandler 4 | import io.grpc.stub.ServerCalls._ 5 | import io.grpc.stub.StreamObserver 6 | 7 | object CallHandlers { 8 | 9 | def unary[Req, Resp](f: Req => Resp): ServerCallHandler[Req, Resp] = { 10 | asyncUnaryCall[Req, Resp](new UnaryMethod[Req, Resp] { 11 | def invoke(req: Req, responseObserver: StreamObserver[Resp]): Unit = { 12 | val response = f(req) 13 | responseObserver.onNext(response) 14 | responseObserver.onCompleted() 15 | } 16 | }) 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /haskell/derivation-examples/src/Lib.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveFunctor #-} 2 | {-# LANGUAGE DerivingVia #-} 3 | 4 | module Lib 5 | where 6 | 7 | import Data.Semigroup 8 | 9 | data User 10 | = LoggedIn { id :: Int, name :: String } 11 | | Anonymous 12 | deriving (Show, Eq) 13 | 14 | user = LoggedIn 123 "Chris" 15 | 16 | data Foo a 17 | = Foo a String 18 | deriving (Functor, Show) 19 | 20 | foo = Foo 42 "hello" 21 | foo' = (+ 1) <$> foo 22 | 23 | newtype Price = Price Int 24 | deriving Semigroup via Sum Int 25 | deriving Show 26 | 27 | someFunc :: IO () 28 | someFunc = do 29 | putStrLn $ show user 30 | putStrLn $ show $ foo' 31 | putStrLn $ show $ (Price 10) <> (Price 20) 32 | -------------------------------------------------------------------------------- /rust/derivation-examples/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | extern crate syn; 3 | 4 | use proc_macro::TokenStream; 5 | 6 | #[proc_macro_derive(ImplementHeight)] 7 | pub fn derive_height(item: TokenStream) -> TokenStream { 8 | let input = syn::parse_macro_input!(item as syn::DeriveInput); 9 | let name = &input.ident; 10 | 11 | /* 12 | * This is a pretty dumb macro that assumes the item 13 | * has a field called "height" whose value is the height in cm. 14 | */ 15 | 16 | format!( 17 | "impl Height for {} {{ 18 | fn height_in_metres(&self) -> f32 {{ 19 | (self.height as f32) / 100.0 20 | }} 21 | }}", 22 | name 23 | ).parse().unwrap() 24 | } 25 | -------------------------------------------------------------------------------- /rust/derivation-examples/src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate derivation_examples; 2 | use derivation_examples::ImplementHeight; 3 | 4 | #[derive(Debug, PartialEq, Eq, ImplementHeight)] 5 | struct Person { 6 | name: String, 7 | height: i32 8 | } 9 | 10 | trait Height { 11 | fn height_in_metres(&self) -> f32; 12 | } 13 | 14 | fn main() { 15 | let chris = Person { name: "Chris".to_string(), height: 180 }; 16 | let yoshiko = Person { name: "Yoshiko".to_string(), height: 148 }; 17 | 18 | // Making use of the derived stock traits: Debug (for {:?}) and Eq 19 | let answer = 20 | if chris == yoshiko { 21 | "yes" 22 | } else { 23 | "no" 24 | }; 25 | println!("Are {:?} and {:?} the same person? {}", chris, yoshiko, answer); 26 | 27 | // Making use of the macro-derived Height impl 28 | println!("Chris's height is {}m", chris.height_in_metres()); 29 | } 30 | -------------------------------------------------------------------------------- /haskell/derivation-examples/package.yaml: -------------------------------------------------------------------------------- 1 | name: derivation-examples 2 | version: 0.1.0.0 3 | github: "githubuser/derivation-examples" 4 | license: BSD3 5 | author: "Author name here" 6 | maintainer: "example@example.com" 7 | copyright: "2020 Author name here" 8 | 9 | # Metadata used when publishing your package 10 | # synopsis: Short description of your package 11 | # category: Web 12 | 13 | # To avoid duplicated efforts in documentation and dealing with the 14 | # complications of embedding Haddock markup inside cabal files, it is 15 | # common to point users to the README.md file. 16 | description: Please see the README on GitHub at 17 | 18 | dependencies: 19 | - base >= 4.7 && < 5 20 | 21 | library: 22 | source-dirs: src 23 | 24 | executables: 25 | derivation-examples-exe: 26 | main: Main.hs 27 | source-dirs: app 28 | ghc-options: 29 | - -threaded 30 | - -rtsopts 31 | - -with-rtsopts=-N 32 | dependencies: 33 | - derivation-examples 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Type class derivation in Scala 3 2 | 3 | This repo contains the code to go with these slides: https://slides.com/cb372/type-class-derivation-in-scala-3 4 | 5 | ## How to run the examples 6 | 7 | ### Haskell 8 | 9 | ``` 10 | cd haskell/derivation-examples 11 | stack run 12 | ``` 13 | 14 | ### Rust 15 | 16 | ``` 17 | cd rust/derivation-examples 18 | cargo run 19 | ``` 20 | 21 | ### Scala 3 Show and Functor examples 22 | 23 | ``` 24 | sbt run 25 | ``` 26 | 27 | ### Scala 3 Mu gRPC server example 28 | 29 | ``` 30 | sbt muService/run 31 | ``` 32 | 33 | 34 | ## Notes on the Functor implementation 35 | 36 | If you look carefully you'll see that `Functor.derived` is a `given`, which makes the use of `derives` completely redundant. `Functor` will be available even if the user doesn't bother to add `derives Functor` to their ADT, which is not great. 37 | 38 | I wanted to rewrite `Functor` to follow the same pattern as `Show`, which is a lot simpler. It avoids the need for `given`, and it handles recursive datatypes nicely without the need for `LazyWrapper`. Unfortunately I couldn't quite get it to compile. You can see my attempt on the `rewrite-functor` branch. 39 | 40 | PRs welcome :) 41 | -------------------------------------------------------------------------------- /src/main/scala/Main.scala: -------------------------------------------------------------------------------- 1 | import mylibrary._ 2 | import myapp._ 3 | 4 | object Main { 5 | 6 | val chris = Person("Chris", 180) 7 | val eric = Pet("Eric", "cat", chris) 8 | val bob = Employee.Grunt("Bob", Employee.Grunt("Alice", Employee.CEO)) 9 | 10 | val foo = Foo(42, "hello") 11 | val oneTwoThree = MyList.Cons(1, MyList.Cons(2, MyList.Cons(3, MyList.Nil))) 12 | 13 | import ExprF._ 14 | val expr = 15 | Add( 16 | Fix(Mult( 17 | Fix(Lit(2)), 18 | Fix(Lit(3)))), 19 | Fix(Lit(4))) 20 | 21 | type Algebra[F[_], A] = F[A] => A 22 | 23 | def cata[F[_]: Functor, A](fix: Fix[F])(alg: Algebra[F, A]): A = 24 | alg(fix.unFix.map(cata(_)(alg))) 25 | 26 | val evaluate: Algebra[ExprF, Int] = { 27 | case Lit(x) => x 28 | case Add(x, y) => x + y 29 | case Mult(x, y) => x * y 30 | } 31 | 32 | def showMe[A: Show](a: A): String = 33 | a.show 34 | 35 | def addOneToEach[F[_]: Functor](fa: F[Int]): F[Int] = fa.map(_ + 1) 36 | 37 | def main(args: Array[String]): Unit = { 38 | println(showMe(chris)) 39 | println(showMe(eric)) 40 | println(showMe(bob)) 41 | println(showMe(Weather.Rainy)) 42 | println(showMe[Weather](Weather.Rainy)) 43 | 44 | println(addOneToEach(oneTwoThree)) 45 | println(addOneToEach(foo)) 46 | println(foo.map(x => s"x is $x")) 47 | println(cata(Fix(expr))(evaluate)) 48 | } 49 | 50 | 51 | } 52 | -------------------------------------------------------------------------------- /haskell/derivation-examples/derivation-examples.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 1.12 2 | 3 | -- This file has been generated from package.yaml by hpack version 0.33.0. 4 | -- 5 | -- see: https://github.com/sol/hpack 6 | -- 7 | -- hash: c7a39e424213195ba124ea421cda36f37261e18e28f34d0df3ef25a025abf3b9 8 | 9 | name: derivation-examples 10 | version: 0.1.0.0 11 | description: Please see the README on GitHub at 12 | homepage: https://github.com/githubuser/derivation-examples#readme 13 | bug-reports: https://github.com/githubuser/derivation-examples/issues 14 | author: Author name here 15 | maintainer: example@example.com 16 | copyright: 2020 Author name here 17 | license: BSD3 18 | build-type: Simple 19 | 20 | source-repository head 21 | type: git 22 | location: https://github.com/githubuser/derivation-examples 23 | 24 | library 25 | exposed-modules: 26 | Lib 27 | other-modules: 28 | Paths_derivation_examples 29 | hs-source-dirs: 30 | src 31 | build-depends: 32 | base >=4.7 && <5 33 | default-language: Haskell2010 34 | 35 | executable derivation-examples-exe 36 | main-is: Main.hs 37 | other-modules: 38 | Paths_derivation_examples 39 | hs-source-dirs: 40 | app 41 | ghc-options: -threaded -rtsopts -with-rtsopts=-N 42 | build-depends: 43 | base >=4.7 && <5 44 | , derivation-examples 45 | default-language: Haskell2010 46 | -------------------------------------------------------------------------------- /mu-service/src/main/scala/myapp/Greeter.scala: -------------------------------------------------------------------------------- 1 | package myapp 2 | 3 | import mu._ 4 | import mu.Encoding.Protobuf 5 | 6 | case class HelloRequest(name: String) 7 | 8 | case class HelloResponse(greeting: String) 9 | 10 | @service(encoding = Protobuf) 11 | trait Greeter derives GrpcServerSide { 12 | 13 | def SayHello(req: HelloRequest): HelloResponse 14 | 15 | } 16 | 17 | object Greeter { 18 | 19 | // Usually these marshallers would be auto-derived by Mu using PBDirect 20 | // but that doesn't exist for Dotty yet 21 | 22 | import java.io._ 23 | 24 | given as ProtobufMarshaller[HelloRequest] { 25 | def stream(value: HelloRequest): InputStream = { 26 | val bytes = Array(10.toByte, value.name.size.toByte) ++ value.name.getBytes 27 | new ByteArrayInputStream(bytes) 28 | } 29 | def parse(stream: InputStream): HelloRequest = { 30 | val in = new DataInputStream(stream) 31 | in.read() 32 | val size = in.read().toInt 33 | val bytes = Array.ofDim[Byte](size) 34 | in.readFully(bytes) 35 | val name = new String(bytes) 36 | HelloRequest(name) 37 | } 38 | } 39 | 40 | given as ProtobufMarshaller[HelloResponse] { 41 | def stream(value: HelloResponse): InputStream = { 42 | val bytes = Array(10.toByte, value.greeting.size.toByte) ++ value.greeting.getBytes 43 | new ByteArrayInputStream(bytes) 44 | } 45 | def parse(stream: InputStream): HelloResponse = { 46 | val in = new DataInputStream(stream) 47 | in.read() 48 | val size = in.read().toInt 49 | val bytes = Array.ofDim[Byte](size) 50 | in.readFully(bytes) 51 | val greeting = new String(bytes) 52 | HelloResponse(greeting) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /rust/derivation-examples/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "derivation_examples" 5 | version = "0.1.0" 6 | dependencies = [ 7 | "syn 1.0.22 (registry+https://github.com/rust-lang/crates.io-index)", 8 | ] 9 | 10 | [[package]] 11 | name = "proc-macro2" 12 | version = "1.0.13" 13 | source = "registry+https://github.com/rust-lang/crates.io-index" 14 | dependencies = [ 15 | "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 16 | ] 17 | 18 | [[package]] 19 | name = "quote" 20 | version = "1.0.6" 21 | source = "registry+https://github.com/rust-lang/crates.io-index" 22 | dependencies = [ 23 | "proc-macro2 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", 24 | ] 25 | 26 | [[package]] 27 | name = "syn" 28 | version = "1.0.22" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | dependencies = [ 31 | "proc-macro2 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", 32 | "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", 33 | "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 34 | ] 35 | 36 | [[package]] 37 | name = "unicode-xid" 38 | version = "0.2.0" 39 | source = "registry+https://github.com/rust-lang/crates.io-index" 40 | 41 | [metadata] 42 | "checksum proc-macro2 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)" = "53f5ffe53a6b28e37c9c1ce74893477864d64f74778a93a4beb43c8fa167f639" 43 | "checksum quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "54a21852a652ad6f610c9510194f398ff6f8692e334fd1145fed931f7fbe44ea" 44 | "checksum syn 1.0.22 (registry+https://github.com/rust-lang/crates.io-index)" = "1425de3c33b0941002740a420b1a906a350b88d08b82b2c8a01035a3f9447bac" 45 | "checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" 46 | -------------------------------------------------------------------------------- /src/main/scala/mylibrary/Show.scala: -------------------------------------------------------------------------------- 1 | package mylibrary 2 | 3 | trait Show[A] { 4 | def (a: A).show: String 5 | } 6 | 7 | object Show { 8 | 9 | import scala.compiletime._ 10 | import scala.deriving.Mirror 11 | 12 | private inline def summonAll[T <: Tuple]: List[Show[_]] = inline erasedValue[T] match { 13 | case _: Unit => Nil 14 | case _: (t *: ts) => summonInline[Show[t]] :: summonAll[ts] 15 | } 16 | 17 | private inline def elemLabels[T <: Tuple]: List[String] = inline erasedValue[T] match { 18 | case _: Unit => Nil 19 | case _: (t *: ts) => constValue[t].asInstanceOf[String] :: elemLabels[ts] 20 | } 21 | 22 | inline def derived[A](using m: Mirror.Of[A]): Show[A] = new Show[A] { 23 | def (a: A).show: String = { 24 | inline m match { 25 | case p: Mirror.ProductOf[A] => showProduct(p, a) 26 | case s: Mirror.SumOf[A] => showSum(s, a) 27 | } 28 | } 29 | } 30 | 31 | inline def showProduct[A](m: Mirror.ProductOf[A], a: A): String = { 32 | val productName = constValue[m.MirroredLabel] 33 | val labels = elemLabels[m.MirroredElemLabels] 34 | val elements = labels.iterator zip a.asInstanceOf[Product].productIterator 35 | val elemInstances = summonAll[m.MirroredElemTypes] 36 | val elemStrings = (elements zip elemInstances).map { 37 | case ((name, value), instance) => 38 | s"$name = ${instance.asInstanceOf[Show[Any]].show(value)}" 39 | } 40 | s"${productName}(${elemStrings.mkString(", ")})" 41 | } 42 | 43 | private inline def rec[A, T](n: Int, ord: Int, a: A): String = { 44 | inline erasedValue[T] match { 45 | case _: (t *: ts) => 46 | if (n == ord) { 47 | summonFrom { 48 | case p: Mirror.ProductOf[`t`] => showProduct[t](p, a.asInstanceOf[t]) 49 | } 50 | } else { 51 | rec[A, ts](n + 1, ord, a) 52 | } 53 | case _: Unit => "" 54 | } 55 | } 56 | 57 | inline def showSum[A](m: Mirror.SumOf[A], a: A): String = { 58 | val ord = m.ordinal(a) 59 | rec[A, m.MirroredElemTypes](0, ord, a) 60 | } 61 | 62 | given as Show[Int] { 63 | def (a: Int).show: String = a.toString 64 | } 65 | 66 | given as Show[String] { 67 | def (a: String).show: String = s""""$a"""" 68 | } 69 | 70 | } 71 | 72 | -------------------------------------------------------------------------------- /haskell/derivation-examples/stack.yaml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by 'stack init' 2 | # 3 | # Some commonly used options have been documented as comments in this file. 4 | # For advanced use and comprehensive documentation of the format, please see: 5 | # https://docs.haskellstack.org/en/stable/yaml_configuration/ 6 | 7 | # Resolver to choose a 'specific' stackage snapshot or a compiler version. 8 | # A snapshot resolver dictates the compiler version and the set of packages 9 | # to be used for project dependencies. For example: 10 | # 11 | # resolver: lts-3.5 12 | # resolver: nightly-2015-09-21 13 | # resolver: ghc-7.10.2 14 | # 15 | # The location of a snapshot can be provided as a file or url. Stack assumes 16 | # a snapshot provided as a file might change, whereas a url resource does not. 17 | # 18 | # resolver: ./custom-snapshot.yaml 19 | # resolver: https://example.com/snapshots/2018-01-01.yaml 20 | resolver: lts-15.13 21 | 22 | # User packages to be built. 23 | # Various formats can be used as shown in the example below. 24 | # 25 | # packages: 26 | # - some-directory 27 | # - https://example.com/foo/bar/baz-0.0.2.tar.gz 28 | # subdirs: 29 | # - auto-update 30 | # - wai 31 | packages: 32 | - . 33 | # Dependency packages to be pulled from upstream that are not in the resolver. 34 | # These entries can reference officially published versions as well as 35 | # forks / in-progress versions pinned to a git hash. For example: 36 | # 37 | # extra-deps: 38 | # - acme-missiles-0.3 39 | # - git: https://github.com/commercialhaskell/stack.git 40 | # commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a 41 | # 42 | # extra-deps: [] 43 | 44 | # Override default flag values for local packages and extra-deps 45 | # flags: {} 46 | 47 | # Extra package databases containing global packages 48 | # extra-package-dbs: [] 49 | 50 | # Control whether we use the GHC we find on the path 51 | # system-ghc: true 52 | # 53 | # Require a specific version of stack, using version ranges 54 | # require-stack-version: -any # Default 55 | # require-stack-version: ">=2.3" 56 | # 57 | # Override the architecture used by stack, especially useful on Windows 58 | # arch: i386 59 | # arch: x86_64 60 | # 61 | # Extra directories used by stack for building 62 | # extra-include-dirs: [/path/to/dir] 63 | # extra-lib-dirs: [/path/to/dir] 64 | # 65 | # Allow a newer minor version of GHC than the snapshot specifies 66 | # compiler-check: newer-minor 67 | ghc-options: 68 | "$locals": -ddump-deriv 69 | -------------------------------------------------------------------------------- /src/main/scala/mylibrary/Functor.scala: -------------------------------------------------------------------------------- 1 | package mylibrary 2 | 3 | trait Functor[F[_]] { 4 | def [A, B] (fa: F[A]).map(f: A => B): F[B] 5 | } 6 | 7 | object Functor { 8 | 9 | import scala.compiletime._ 10 | import scala.deriving.Mirror 11 | 12 | type Id[t] = t 13 | type Const[c] = [t] =>> c 14 | 15 | given Functor[Id] { 16 | def [A, B] (a: A).map(f: A => B): B = f(a) 17 | } 18 | 19 | given [T] as Functor[Const[T]] { 20 | def [A, B] (t: T).map(f: A => B): T = t 21 | } 22 | 23 | object Helpers { 24 | 25 | inline def summonAsArray[T <: Tuple]: Array[Any] = 26 | summonAsArray0[T](0, new Array[Any](constValue[Tuple.Size[T]])) 27 | 28 | inline def summonAsArray0[T](i: Int, arr: Array[Any]): Array[Any] = inline erasedValue[T] match { 29 | case _: Unit => arr 30 | case _: (a *: b) => 31 | arr(i) = summonInline[a] 32 | summonAsArray0[b](i+1, arr) 33 | } 34 | 35 | final class ArrayProduct(val elems: Array[Any]) extends Product { 36 | def canEqual(that: Any): Boolean = true 37 | def productElement(n: Int) = elems(n) 38 | def productArity = elems.length 39 | override def productIterator: Iterator[Any] = elems.iterator 40 | } 41 | 42 | type PolyMirror[C, O[_]] = C { type MirroredType = O ; type MirroredElemTypes[_] } 43 | type MirrorOf[O[_]] = PolyMirror[Mirror, O] 44 | type ProductMirrorOf[O[_]] = PolyMirror[Mirror.Product, O] 45 | type CoproductMirrorOf[O[_]] = PolyMirror[Mirror.Sum, O] 46 | 47 | case class Wrap[T](t: T) 48 | class Dummy 49 | type Apply[F[_]] = F[Dummy] 50 | type Unapply[T] = T match { 51 | case Wrap[Apply[a]] => Functor[a] 52 | case Wrap[Dummy] => Functor[Id] 53 | case Wrap[c] => Functor[Const[c]] 54 | } 55 | 56 | type Functors[T[_]] = Functors0[Apply[T]] 57 | 58 | type Functors0[T] <: Tuple = T match { 59 | case Unit => Unit 60 | case (a *: b) => Unapply[Wrap[a]] *: Functors0[b] 61 | } 62 | 63 | } 64 | 65 | import Helpers._ 66 | 67 | case class LazyWrapper[F[_]](functor: Functor[F]) 68 | 69 | given derived[F[_]](using wrapper: => LazyWrapper[F]) as Functor[F] { 70 | def [A, B] (fa: F[A]).map(f: A => B): F[B] = 71 | wrapper.functor.map(fa)(f) 72 | } 73 | 74 | inline given [F[_]](using mirror: MirrorOf[F]) as LazyWrapper[F] = { 75 | val functors = summonAsArray[Functors[mirror.MirroredElemTypes]] 76 | inline mirror match { 77 | case p: ProductMirrorOf[F] => derivedForProduct[F](p, functors) 78 | case c: CoproductMirrorOf[F] => derivedForCoproduct[F](c, functors) 79 | } 80 | } 81 | 82 | inline def derivedForProduct[F[_]](m: ProductMirrorOf[F], elemFunctors: Array[Any]): LazyWrapper[F] = 83 | LazyWrapper( 84 | new Functor[F] { 85 | def [A, B] (fa: F[A]).map(f: A => B): F[B] = { 86 | val n = elemFunctors.length 87 | if (n == 0) fa.asInstanceOf[F[B]] 88 | else { 89 | val arr = new Array[Any](n) 90 | var i = 0 91 | while(i < n) { 92 | val F: Functor[_] = elemFunctors(i).asInstanceOf 93 | val elem: Any = fa.asInstanceOf[Product].productElement(i) 94 | arr(i) = F.map(elem.asInstanceOf)(f) 95 | i = i+1 96 | } 97 | m.fromProduct(ArrayProduct(arr)).asInstanceOf[F[B]] 98 | } 99 | } 100 | } 101 | ) 102 | 103 | inline def derivedForCoproduct[F[_]](m: CoproductMirrorOf[F], elemFunctors: Array[Any]): LazyWrapper[F] = 104 | LazyWrapper( 105 | new Functor[F] { 106 | def [A, B] (fa: F[A]).map(f: A => B): F[B] = { 107 | val i = m.ordinal(fa.asInstanceOf) 108 | elemFunctors(i).asInstanceOf[Functor[F]].map(fa)(f) 109 | } 110 | } 111 | ) 112 | 113 | } 114 | -------------------------------------------------------------------------------- /mu/src/main/scala/mu/GrpcServerSide.scala: -------------------------------------------------------------------------------- 1 | package mu 2 | 3 | import io.grpc._ 4 | import io.grpc.MethodDescriptor.Marshaller 5 | 6 | trait GrpcServerSide[S] { 7 | 8 | def (service: S).toServerServiceDefinition: ServerServiceDefinition 9 | 10 | } 11 | 12 | object GrpcServerSide { 13 | 14 | inline def derived[S]: GrpcServerSide[S] = ${ gen[S] } 15 | 16 | import scala.quoted._ 17 | import scala.reflect._ 18 | 19 | def gen[S](using qctx: QuoteContext, s: Type[S]): Expr[GrpcServerSide[S]] = { 20 | import qctx.tasty._ 21 | 22 | def isTraitOrAbstractClass(flags: Flags) = 23 | flags.is(Flags.Trait) || flags.is(Flags.Abstract) 24 | 25 | def hasServiceAnnotation(annots: List[Term]) = 26 | annots.exists(_.tpe <:< typeOf[mu.service]) 27 | 28 | def isValidMethod(tparams: List[TypeDef], paramss: List[List[ValDef]], rhs: Option[Term]) = 29 | tparams.isEmpty && paramss.flatten.size == 1 && rhs.isEmpty 30 | 31 | def sequence(exprs: List[Expr[S => ServerMethodDefinition[_, _]]]): Expr[List[S => ServerMethodDefinition[_, _]]] = exprs match { 32 | case h :: t => '{ $h :: ${sequence(t)} } 33 | case Nil => '{ Nil: List[S => ServerMethodDefinition[_, _]] } 34 | } 35 | 36 | typeOf[S].classSymbol match { 37 | case Some(classSym) if isTraitOrAbstractClass(classSym.flags) && hasServiceAnnotation(classSym.annots) => 38 | /* 39 | * In the real Mu we would grab the encoding and other configuration 40 | * from the @service annotation. 41 | * For the demo we just assume protobuf. 42 | */ 43 | 44 | val serviceName = Expr(classSym.fullName) 45 | 46 | /* 47 | * For each method in the trait, we build a gRPC method definition: 48 | * - construct a ServerCallHandler that, given an instance of the service trait 49 | * and a request, calls the method on the instance to get the response 50 | * - Summon marshallers for the method's request and response types 51 | * - generate the appropriate fully-qualified name ("package.serviceName/methodName") 52 | */ 53 | val methodDefinitions: List[Expr[S => ServerMethodDefinition[_, _]]] = 54 | classSym.classMethods.map { methodSym => 55 | methodSym.tree match { 56 | case DefDef(methodName, tparams, paramss, returnTpt, rhs) 57 | if isValidMethod(tparams, paramss, rhs) => 58 | 59 | val requestTpe = paramss.flatten.head.tpt.tpe 60 | val responseTpe = returnTpt.tpe 61 | 62 | (requestTpe.seal, responseTpe.seal) match { 63 | case ('[$req], '[$resp]) => 64 | val serverCallHandler: Expr[S => ServerCallHandler[$req, $resp]] = 65 | '{ (srv: $s) => 66 | CallHandlers.unary[$req, $resp](request => 67 | ${ 68 | Apply( 69 | Select('srv.unseal, methodSym), 70 | List('request.unseal) 71 | ).seal.cast 72 | } 73 | ) 74 | } 75 | 76 | val requestMarshaller: Expr[Marshaller[$req]] = 77 | Expr.summon[ProtobufMarshaller[$req]] match { 78 | case Some(marshaller) => 79 | marshaller 80 | case None => 81 | Reporting.throwError(s"Could not summon a Protobuf marshaller for request type $req") 82 | } 83 | 84 | val responseMarshaller: Expr[Marshaller[$resp]] = 85 | Expr.summon[ProtobufMarshaller[$resp]] match { 86 | case Some(marshaller) => 87 | marshaller 88 | case None => 89 | Reporting.throwError(s"Could not summon a Protobuf marshaller for response type $resp") 90 | } 91 | 92 | val fullMethodName = Expr(s"${classSym.fullName}/$methodName") 93 | 94 | val methodDescriptor: Expr[MethodDescriptor[$req, $resp]] = '{ 95 | MethodDescriptor 96 | .newBuilder($requestMarshaller, $responseMarshaller) 97 | .setType(MethodDescriptor.MethodType.UNARY) 98 | .setFullMethodName($fullMethodName) 99 | .build() 100 | } 101 | 102 | '{ (srv: $s) => 103 | ServerMethodDefinition.create($methodDescriptor, $serverCallHandler(srv)) 104 | } 105 | } 106 | } 107 | } 108 | 109 | // Turn a list of expressions into an expression of a list 110 | val methodDefinitionsExpr: Expr[List[S => ServerMethodDefinition[_, _]]] = 111 | sequence(methodDefinitions) 112 | 113 | '{ 114 | // this line is a workaround for https://github.com/lampepfl/dotty/issues/9020 115 | type X = $s 116 | new GrpcServerSide[X]{ 117 | def (service: S).toServerServiceDefinition: ServerServiceDefinition = { 118 | val builder = ServerServiceDefinition.builder($serviceName) 119 | ${methodDefinitionsExpr}.foreach { f => 120 | builder.addMethod(f(service)) 121 | } 122 | builder.build() 123 | } 124 | } 125 | } 126 | case _ => 127 | Reporting.throwError("Can only derive GrpcServerSide for @service-annotated traits or abstract classes") 128 | } 129 | } 130 | 131 | 132 | } 133 | 134 | --------------------------------------------------------------------------------