Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save martinsandredev/abfd126451dd1656fabee2de7c673901 to your computer and use it in GitHub Desktop.
Save martinsandredev/abfd126451dd1656fabee2de7c673901 to your computer and use it in GitHub Desktop.
Comparison between Scala and Haskell - Pt. 1

Comparison between Scala and Haskell - Pt. 1

Algebraic Data Types

Scala:

enum Status:
  case Ok
  case Error(err: String)

export Status.*

def status: Status = Ok

def status: Status = Error("Bad!")
enum HttpResponse[+A]:
  case NotFound extends HttpResponse[Nothing]
  case Ok[A](res: A) extends HttpResponse[A]

export HttpResponse.*

def response: HttpResponse[String] = Ok("Response")

def response: HttpResponse[String] = NotFound

Haskell:

data Status = Ok | Error String

status :: Status
status = Ok

status :: Status
status = Error "Bad!"
data HttpResponse a = NotFound | Ok a

response :: HttpResponse String
response = Ok "Response"

response :: HttpResponse String
response = NotFound

Newtypes

Scala:

object Name:
  opaque type Name = String

  def mkName(s: String): Either[String, Name] = Either.cond(
    s.length >= 2 && s.length <= 50,
    s,
    "Invalid name"
  )

import Name.*

def doStuff(name: Name) = ()

doStuff("")
// Found:    ("" : String)
// Required: Name.Name

mkName("André") match
  case Right(name) => doStuff(name)
  case _ => ()

Haskell:

module Name
  ( Name
  , mkName
  )
  where

newtype Name =
  Name String
  deriving (Eq, Show)

mkName :: String -> Either String Name
mkName s
  | l < 2 || l > 50 = Left "Invalid name"
  | otherwise = Right $ Name s
  where
    l = length s

module OtherModule

import Name

doStuff :: Name -> ()
doStuff = const ()

doStuff ""
-- Couldn't match expected type ‘Name’ with actual type ‘[Char]’

case mkName "Andre" of
  Right name -> doStuff name
  _ -> ()

Side-effect tracking and monadic composition

Scala:

import cats.effect.*
import cats.implicits.*

def printStuff: String => IO[Unit] = IO.println

def getStuff: IO[String] = IO.pure("stuff")

def doStuff: IO[Unit] = getStuff >>= printStuff

Haskell:

printStuff :: String -> IO ()
printStuff = putStrLn

getStuff :: IO String
getStuff = return "stuff"

doStuff :: IO ()
doStuff = getStuff >>= printStuff

Do notation

Scala:

def doStuff: IO[String] = for
  s <- getStuff
  reverted = s.reverse
  _ <- printStuff(reverted)
yield reverted

Haskell:

doStuff :: IO String
doStuff = do
  s <- getStuff
  let reverted = reverse s
  _ <- printStuff reverted
  return reverted

// Or
...
doStuff = do
  ...
  printStuff reverted -- Discard unnecessary result
  return reverted

Higher-Kinded Types and Type Classes

Scala:

trait UserDsl[F[_]]:
  def findUserByName(name: String): F[Option[User]]
  def saveUser(user: User): F[Unit]

object UserDsl:
  def apply[F[_]](implicit userDsl: UserDsl[F]): UserDsl[F] = userDsl

def doStuffWithName[F[_]: Sync: UserDsl](name: String): F[Unit] = for
  maybeUser <- UserDsl[F].findUserByName(name)
  _ <- maybeUser match
    case Some(user) => UserDsl[F].saveUser(user)
    case _ => Sync[F].raiseError(Error(s"User not found with name: $name"))
yield ()

implicit def userDsl: UserDsl[IO] = new UserDsl[IO]:
  override def findUserByName(name: String) = ...

Haskell:

class UserDsl m where
  findUserByName :: String -> m (Maybe User)
  saveUser :: User -> m ()

doStuffWithName :: (MonadError e m, UserDsl m) => String -> m ()
doStuffWithName name = do
  maybeUser <- findUserByName name
  case maybeUser of
    Just user -> saveUser user
    _ -> throwError $ error $ "User not found with name: " ++ name

instance UserDsl IO where
  findUserByName name = ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment