How the Functional Ability of Scala Comparing to Haskell - an Example

About one and half year ago, it was my first time to consider Scala seriously, I wrote a blog about the syntax example of Scala, Erlang and Haskell .

With more experience of Scala, I'd like to know how about the functional ability of Scala comparing to Haskell. I picked up  Paul R. Brown's perpubplat blog engine again, which is a Haskell implementation heavily in functional style. I tried to port more code from Haskell to Scala keeping in similar expressions. Here's the code example of Entry.scala in Scala comparing to Brown's original Entry.hs:

Original Haskell code piece

-- | Data structures for an item (post or comment) and the
-- overall structure in terms of parents and children.
module Blog.Model.Entry where

import qualified Blog.FrontEnd.Urls as U
import Utilities
import qualified Blog.Constants as C

import Maybe
import List ( sortBy, isPrefixOf, intersperse)
import qualified Data.Map as M
import Data.Map ( (!) )

type ISO8601DatetimeString = String
type XhtmlString = String

-- | Overall data model for the runtime.  
data Model = Model { -- | 
                     by_permatitle :: M.Map String Item,
                     by_int_id :: M.Map Int Item,
                     child_map :: M.Map Int [Int],
                     all_items :: [Item],
                     next_id :: Int }

empty :: Model
empty = Model M.empty M.empty M.empty [] 0

data Kind = Post | Comment | Trackback
            deriving (Show, Read, Eq)

build_model :: [Item] -> Model
build_model [] = empty
build_model items = Model (map_by permatitle sorted_items)
                    bid                    
                    (build_child_map sorted_items)
                    (sorted_items)
                    (n+1)
    where
      sorted_items = sort_by_created_reverse items
      bid = (map_by internal_id sorted_items)
      n = fst . M.findMax $ bid

build_child_map :: [Item] -> M.Map Int [Int]
build_child_map i = build_child_map_ (M.fromList $ (map (\x -> (internal_id x,[])) i)) i

-- Constructed to take advantage of the input being in sorted order.
build_child_map_ :: M.Map Int [Int] -> [Item] -> M.Map Int [Int]
build_child_map_ m [] = m
build_child_map_ m (i:is) = if (parent i == Nothing) then
                                build_child_map_ m is
                            else
                                build_child_map_ (M.insertWith (++) (unwrap $ parent i) [internal_id i] m) is

-- | Insert an item, presuming that all of its data other than
-- internal identifier have been correctly set.
insert :: Model -> Item -> (Item,Model)
insert m i = (i', m { by_permatitle = M.insert (permatitle i') i' $ by_permatitle m
                    , by_int_id = M.insert n i' $ by_int_id m
                    , child_map = M.insert (internal_id i') [] $
                                  case parent i of
                                    Nothing ->
                                        child_map m
                                    (Just p_id) ->
                                        M.insert p_id (insert_comment_ m (item_by_id m p_id) i') $ child_map m
                    , all_items = insert_ after (all_items m) i'
                    , next_id = n + 1 } )
    where
      n = next_id m
      i' = i { internal_id = n }

insert_comment_ :: Model -> Item -> Item -> [Int]
insert_comment_ m p c = map internal_id (insert_ before (children m p) c)

insert_ :: (Item -> Item -> Bool) -> [Item] -> Item -> [Item]
insert_ _ [] y = [y]
insert_ o s@(x:xs) y = if (x `o` y) then
                           (x:(insert_ o xs y))
                       else
                           (y:s)

after :: Item -> Item -> Bool
after a b = (created a) > (created b)

before :: Item -> Item -> Bool
before a b = (created a) < (created b)

-- | Apply a structure-preserving function, i.e., one that does not
-- change parent/child relationships or ids, to a specific item.
alter :: (Item -> Item) -> Model -> Item -> IO Model
alter f m i = do { ts <- now
                 ; let i' = (f i) { updated = ts }
                 ; return $ m { by_permatitle = M.insert (permatitle i') i' $ by_permatitle m
                              , by_int_id = M.insert (internal_id i') i' $ by_int_id m
                              , child_map = if (parent i == Nothing) then
                                                child_map m
                                            else
                                                M.insert p_id resort_siblings $ child_map m
                              , all_items = insert_ after all_but i' } }
    where
      not_i = \item -> (internal_id item) /= (internal_id i)
      all_but = filter not_i $ all_items m
      p_id = unwrap $ parent i
      p = item_by_id m p_id
      resort_siblings = map internal_id (insert_ before (filter not_i $ children m p) i)

cloak :: Model -> Item -> IO Model
cloak = alter (\i -> i { visible = False })

uncloak :: Model -> Item -> IO Model
uncloak = alter (\i -> i { visible = True })

permatitle_exists :: Model -> String -> Bool
permatitle_exists = (flip M.member) . by_permatitle

max_id :: Model -> Int
max_id = fst . M.findMax . by_int_id

post_by_permatitle :: Model -> String -> Item
post_by_permatitle = (!) . by_permatitle

maybe_post_by_permatitle :: Model -> String -> Maybe Item
maybe_post_by_permatitle = (flip M.lookup) . by_permatitle

item_by_id :: Model -> Int -> Item
item_by_id = (!) . by_int_id

children :: Model -> Item -> [Item]
children m i = map (item_by_id m) ((child_map m) ! (internal_id i))

unwrap :: Maybe a -> a
unwrap (Just x) = x
unwrap Nothing = error "Can't unwrap nothing!"

data Author = Author { name :: String,
                       uri :: Maybe String,
                       email :: Maybe String,
                       show_email :: Bool
                     }
              deriving ( Show,Read,Eq )

-- | General purpose runtime data structure for holding a post or
-- comment.  For a comment, a number of the fields will be ignored
-- (e.g., comments and tags) until/if the presentation and syndication
-- system gets fancier.
data Item = Item { -- | an internal unique number for this post
                   internal_id :: Int,
                   -- | the kind of item that this represents
                   kind :: Kind,
                   -- | the title of the post, as it should be rendered on
                   -- the web or inserted in an Atom feed; this should be a
                   -- valid XHTML fragment.
                   title :: XhtmlString,
                   -- | the summary of the post, as it should be rendered on
                   -- the web or intersted into an Atom feed; this should be
                   -- a valid XHTML fragment.
                   summary :: Maybe XhtmlString,
                   -- | the body of the post as an XHTML fragment.  This
                   -- will be wrapped in an XHTML @<div>@ when rendered on
                   -- the web or in a feed.
                   body :: XhtmlString,
                   -- | tags for the post, if any, expected to be in
                   -- alphabetical order and consisting of letters, digits,
                   -- dashes, and/or underscores.
                   tags :: [String],
                   -- | a generated UID for the post; this is expected to be
                   -- suitable for use as an Atom GUID.  The expectation is
                   -- that it will be supplied by the implementation when
                   -- the post is ingested.
                   uid :: String,
                   -- | a permanent title for the item, consisting of only
                   -- lowercase letters, digits, and dashes.
                   permatitle :: String,
                   -- | the timestamp, as an ISO8601 datetime, when the post
                   -- came into being.  This is never blank and would be
                   -- supplied by the implementation when the post is
                   -- ingested.
                   created :: ISO8601DatetimeString,
                   -- | the timestamp, as an ISO8601 datetime, when the post
                   -- was updated.  Initially, this is equal to the value of
                   -- the 'created' field.
                   updated :: ISO8601DatetimeString,
                   -- | the author of the post, expected to be hardwired to
                   -- the author of the blog
                   author :: Author,
                   -- | whether or not the item is to be displayed.
                   visible :: Bool,
                   -- | this item's parent, if any.
                   parent :: Maybe Int
                 }
            deriving ( Show, Read, Eq )

-- | Compute a permalink for the item relative to the supplied base URL.
permalink :: Model
          -> Item -- ^ the item
          -> String
permalink m i = U.post (relative_url m i)

relative_url :: Model -> Item -> String
relative_url m = _form_permalink . (ancestors m)

_form_permalink :: [Item] -> String
_form_permalink [] = ""
_form_permalink [i] =  let s = permatitle i in 
                       if (kind i == Post) then
                           "/" ++ s
                       else
                           "#" ++ s
_form_permalink (i:is) = if (kind i == Post) then
                            ("/" ++ permatitle i) ++ (_form_permalink is)
                        else 
                            (_form_permalink is)


ancestor_path :: Model -> Item -> String
ancestor_path m i = concat . (intersperse "/") . (map permatitle) $ ancestors m i

ancestors :: Model -> Item -> [Item]
ancestors m i = ancestors_ m [] (Just $ internal_id i)

ancestors_ :: Model -> [Item] -> Maybe Int -> [Item]
ancestors_ _ is Nothing = is
ancestors_ m is (Just i) = ancestors_ m (i':is) (parent i')
    where
      i' = item_by_id m i

lastUpdated :: [Item] -> ISO8601DatetimeString
lastUpdated ps = maximum (map updated ps)

drop_invisible :: [Item] -> [Item]
drop_invisible = filter visible

sort_by_created :: [Item] -> [Item]
sort_by_created = sortBy created_sort

created_sort :: Item -> Item -> Ordering
created_sort a b = compare (created a) (created b)

sort_by_created_reverse :: [Item] -> [Item]
sort_by_created_reverse = sortBy created_sort_reverse

created_sort_reverse :: Item -> Item -> Ordering
created_sort_reverse a b = compare (created b) (created a)

-- | Filter a list of items according to a date fragment
date_fragment_filter_ :: ISO8601DatetimeString -> [Item] -> [Item]
date_fragment_filter_ s = filter ((s `isPrefixOf`) . created)

-- | Filter a list of posts for those made in a specific year.
year_filter :: Int -- ^ year
            -> [Item] -> [Item]
year_filter y = date_fragment_filter_ $ show y

-- | Filter a list of posts for those made in a specific month.              
month_filter :: Int -- ^ year
             -> Int -- ^ month
             -> [Item] -> [Item]
month_filter y m | (0 < m) && (m < 13) = date_fragment_filter_ ((show y) ++ (pad_ m))
                 | otherwise = const []

-- | Filter a list of posts for those made on a specific day
day_filter :: Int -- ^ year
           -> Int -- ^ month
           -> Int -- ^ day
           -> [Item] -> [Item]
day_filter y m d = date_fragment_filter_ ((show y) ++ (pad_ m) ++ (pad_ d))

-- | Utility function to zero pad months and days in date expressions.
pad_ :: Int -> String
pad_ i | i < 10 = "-0" ++ (show i)
       | otherwise = ('-':(show i))
              
-- to do: make this faster using the sortedness.
tags_filter :: [String] -> [Item] -> [Item]
tags_filter t p = foldl (flip ($)) p (map tag_filter t)

tag_filter :: String -> [Item] -> [Item]
tag_filter t = filter ((t `elem`) . tags) 

plink_filterf :: String -> Item -> Bool
plink_filterf = flip $ (==) . permatitle

plink_filter :: String -> [Item] -> [Item]
plink_filter = filter . plink_filterf

ymd_plink_finder :: Int -> Int -> Int -> String -> [Item] -> [Item]
ymd_plink_finder y m d t = (plink_filter t) . (day_filter y m d)

all_posts :: Model -> [Item]
all_posts = (filter (\x -> Post == kind x)) . all_items

all_comments :: Model -> [Item]
all_comments = (filter (\x -> Comment == kind x)) . all_items

flatten :: Model -> [Item] -> [Item]
flatten m = flatten_ (children m)

flatten_ :: (a -> [a]) -> [a] -> [a]
flatten_ _ [] = []
flatten_ f (i:is) = (i:(flatten_ f (f i))) ++ (flatten_ f is)

concat_comments :: Model -> [Item] -> [Item]
concat_comments m = (foldr (++) []) . (map $ children m)

(</>) :: String -> String -> String
s </> t = s ++ ('/':t)

to_string :: Item -> String
to_string i = concat [metadata i, "\n", body_block i, "\n", summary_block i]

metadata :: Item -> String
metadata i = unlines $ apply i [ ("internal_id",show . internal_id),
                                 ("parent", show . parent),
                                 ("title",title),
                                 ("tags",show_no_quotes . tags),
                                 ("permatitle",permatitle),
                                 ("kind",show . kind),
                                 ("uid",uid),
                                 ("created",created),
                                 ("updated",updated),
                                 ("author",show . author),
                                 ("visible",show . visible) ]

show_no_quotes :: [String] -> String
show_no_quotes = concat . (intersperse ", ")

apply :: Item -> [(String,(Item -> String))] -> [String]
apply _ [] = []
apply i (x:xs) = ((concat [fst x, ": ", (snd x) i]) : (apply i xs))

body_block :: Item -> String
body_block i = concat ["--- START BODY ---\n",
                       (body i),
                       "\n--- END BODY ---\n"]

summary_block :: Item -> String
summary_block i | summary i == Nothing = ""
                | otherwise = concat ["--- START SUMMARY ---\n",
                                      (unwrap $ summary i),
                                      "\n--- END SUMMARY ---\n"]

default_author :: Author
default_author = Author C.author_name C.author_uri C.author_email True

In Scala:

  • Formatted and highlighted by NetBeans Scala Plugin, exported via [File] -> [Print to HTML ...]
package org.aiotrade.blog.model

import java.util.Calendar
import org.aiotrade.blog.{Constants => C}

object Entry {
  type XhtmlString = String

  class Model (
    var by_permatitle: Map[String, Item],
    var by_int_id: Map[Int, Item],
    var child_map: Map[Int, List[Int]],
    var all_items: List[Item],
    var next_id: Int
  ) {
    // call by name
    def apply(block: => Unit) = {block; this}
  }

  abstract class Kind //deriving (Show, Read, Eq)
  case object Post extends Kind
  case object Comment extends Kind
  case object Trackback extends Kind

  case class Author (
    var name: String,
    var uri : Option[String] = None,
    var email: Option[String] = None,
    var show_email: Boolean = false
  )

  /** General purpose runtime data structure for holding a post or
   * comment.  For a comment, a number of the fields will be ignored
   * (e.g., comments and tags) until/if the presentation and syndication
   * system gets fancier.
   */
  case class Item (
    // an internal unique number for this post
    var internalId: Int,
    // the kind of item that this represents
    var kind: Kind,
    // the title of the post, as it should be rendered on
    // the web or inserted in an Atom feed; this should be a
    // valid XHTML fragment.
    var title: XhtmlString,
    // the summary of the post, as it should be rendered on
    // the web or intersted into an Atom feed; this should be
    // a valid XHTML fragment.
    var summary: Option[XhtmlString],
    // the body of the post as an XHTML fragment.  This
    // will be wrapped in an XHTML @<div>@ when rendered on
    // the web or in a feed.
    var body: XhtmlString,
    // tags for the post, if any, expected to be in
    // alphabetical order and consisting of letters, digits,
    // dashes, and/or underscores.
    var tags: List[String],
    // a generated UID for the post; this is expected to be
    // suitable for use as an Atom GUID.  The expectation is
    // that it will be supplied by the implementation when
    // the post is ingested.
    var uid: String,
    // a permanent title for the item, consisting of only
    // lowercase letters, digits, and dashes.
    var permatitle: String,
    // the timestamp, as an ISO8601 datetime, when the post
    // came into being.  This is never blank and would be
    // supplied by the implementation when the post is
    // ingested.
    var created: Long,
    // the timestamp, as an ISO8601 datetime, when the post
    // was updated.  Initially, this is equal to the value of
    // the 'created' field.
    var updated: Long,
    //the author of the post, expected to be hardwired to
    // the author of the blog
    var author: Author,
    //whether or not the item is to be displayed.
    var visible: Boolean,
    //this item's parent, if any.
    var parent: Option[Int]
  ) {
    def apply(block: Item => Unit) = {block(this); this}
  }

  def empty = new Model(Map(), Map(), Map(), Nil, 0)

  def build_model(is: List[Item]) =
    is match {
      case Nil => empty
      case _ =>
        val sortedIs = sort_by_created_reverse(is)
        val bid = Map() ++ sortedIs.map{x => (x.internalId -> x)}
        val n = bid.keySet.max

        new Model(Map() ++ sortedIs.map{x => (x.permatitle -> x)},
                  bid,
                  buildChildMap(sortedIs),
                  sortedIs,
                  n + 1)
    }

  def buildChildMap(is: List[Item]) =
    buildChildMap_(Map() ++ is.map(_.internalId -> Nil), is)

  def buildChildMap_(map: Map[Int, List[Int]], is: List[Item]) =
    map ++ {
      for (i <- is if i.parent.isDefined) yield {
        // pid, cids definitions go into body // it's more efficient.
        val pid = i.parent.get
        val cids = map.getOrElse(pid, Nil)
        pid -> (i.internalId :: cids)
      }
    }

  /** Insert an item, presuming that all of its data other than
   internal identifier have been correctly set.
   */
  def insert(m: Model, i: Item): (Item, Model) = {
    val n = m.next_id
    i.internalId = n

    (i, m {
        m.by_permatitle += (i.permatitle -> i)
        m.by_int_id += (n -> i)
        m.child_map = (i.parent match {
            case None => m.child_map
            case Some(p_id) => m.child_map + (p_id -> (insert_comment_(m, item_by_id(m)(p_id), i)))
          })
        m.all_items = insert_(after, m.all_items, i)
        m.next_id = n + 1 }
    )
  }

  def insert_comment_(m: Model, p: Item, c: Item): List[Int] = 
    insert_(before, children(m)(p), c) map (_.internalId)

  def insert_(o: (Item, Item) => Boolean, is: List[Item], y: Item): List[Item] =
    is match {
      case Nil => List(y)
      case x :: xs => if (o(x, y)) x :: insert_(o, xs, y) else (y :: is)
    }
  
  def after (a: Item, b: Item): Boolean =
    a.created > b.created

  def before(a: Item, b: Item): Boolean =
    a.created < b.created

  /**
   * Apply a structure-preserving function, i.e., one that does not
   * change parent/child relationships or ids, to a specific item.
   */
  def alter(f: (Item => Item), m: Model, i: Item): Model = {// -> IO Model
    val not_i = (item: Item) => item.internalId != i.internalId
    val all_but = m.all_items filter not_i
    val p_id = unwrap (i.parent)
    val p = item_by_id(m)(p_id)
    val resort_siblings = insert_(before, children(m)(p) filter not_i, i) map (_.internalId)

    val ts = System.currentTimeMillis
    val i1 = f(i) {_.updated = ts}
    m {
      m.by_permatitle += (i1.permatitle -> i1)
      m.by_int_id += (i1.internalId -> i1)
      m.child_map = i.parent match {
        case None => m.child_map
        case _ => m.child_map + (p_id -> resort_siblings)
      }
      m.all_items = insert_(after, all_but, i1)
    }
  }

  def cloak(m: Model, i: Item): Model = // -> IO Model
    alter (i => i {_.visible = false}, m, i)

  def uncloak(m: Model, i: Item): Model = // -> IO Model
    alter (i => i {_.visible = true}, m, i)

  def permatitle_exists(m: Model, p: String): Boolean =
    m.by_permatitle.contains(p)

  def max_id(m: Model): Int =
    m.by_int_id.keySet.max

  def post_by_permatitle(m: Model, p: String): Item =
    m.by_permatitle(p)

  def maybe_post_by_permatitle(m: Model, p: String): Option[Item] =
    m.by_permatitle.get(p)

  def item_by_id(m: Model)(id: Int): Item = 
    m.by_int_id(id)

  def children(m: Model)(i: Item): List[Item] =
    m.child_map(i.internalId) map (item_by_id(m))

  def unwrap[T](a: Option[T]): T =
    a match {
      case Some(x) => x
      case None => error("Can't unwrap none!")
    }

  def relative_url(m: Model, i: Item): String =
    _form_permalink(ancestors(m, i))

  def _form_permalink(is: List[Item]): String =
    is match {
      case Nil => ""
      case i :: Nil =>
        val s = i.permatitle
        if (i.kind == Post)
          "/" + s
        else
          "#" + s
      case i :: is =>
        if (i.kind == Post)
          ("/" + i.permatitle) + _form_permalink(is)
        else
          _form_permalink(is)
    }

  def ancestors(m: Model, i: Item): List[Item] =
    ancestors_(m, Nil, Some(i.internalId))

  def ancestors_(m: Model, is: List[Item], i_? : Option[Int]): List[Item] =
    i_? match {
      case None => is
      case Some(i) =>
        val i1 = item_by_id(m)(i)
        ancestors_(m, i1 :: is, i1.parent)
    }

  def lastUpdated(ps: List[Item]): Long =
    ps map (_.updated) max

  def drop_invisible(is: List[Item]): List[Item] =
    is filter (_.visible)

  def sort_by_created(is: List[Item]): List[Item] =
    is sortWith created_sort _

  def created_sort(a: Item, b: Item) =
    a.created < b.created

  def sort_by_created_reverse(is: List[Item]): List[Item] =
    is sortWith created_sort_reverse _

  def created_sort_reverse(a: Item, b: Item) =
    b.created < a.created

  def date_fragment_filter_(is: List[Item], ts: Int*) = {
    val cal = Calendar.getInstance
    ts match {
      case Seq(y, m, d) => is filter {i => pad_(cal, i.created) match {
            case (`y`, `m`, `d`) => true
            case _ => false
          }
        }
      case Seq(y, m) => is filter {i => pad_(cal, i.created) match {
            case (`y`, `m`, _) => true
            case _ => false
          }
        }
      case Seq(y) => is filter {i => pad_(cal, i.created) match {
            case (`y`, _, _) => true
            case _ => false
          }
        }
    }
  }
  
  def year_filter(y: Int)(is: List[Item]): List[Item] =
    date_fragment_filter_(is, y)

  def month_filter(y: Int, m: Int)(is: List[Item]): List[Item] =
    date_fragment_filter_(is, y, m)

  def day_filter(y: Int, m: Int, d: Int)(is: List[Item]): List[Item] =
    date_fragment_filter_(is, y, m, d)

  def pad_(cal: Calendar, t: Long): (Int, Int, Int) = {
    cal.setTimeInMillis(t)
    import Calendar._
    (cal.get(YEAR), cal.get(MONTH) + 1, cal.get(DAY_OF_MONTH))
  }

  def tags_filter(ts: List[String])(is: List[Item]): List[Item] =
    is filter (i => (false /: ts) {_ || i.tags.contains(_)})

  def tag_filter(t: String)(is: List[Item]): List[Item] =
    is filter (_.tags.contains(t))

  def plink_filterf(p: String)(i: Item): Boolean =
    i.permatitle == p

  def plink_filter(p: String)(is: List[Item]): List[Item] =
    is filter plink_filterf(p)

  def ymd_plink_finder(y: Int, m: Int, d: Int, p: String)(is: List[Item]): List[Item] =
    plink_filter(p) (day_filter(y, m, d) (is))

  def </> (s: String, t: String): String =
    s + '/' + t

  def all_posts(m: Model): List[Item] =
    m.all_items filter (_.kind == Post)

  def all_comments(m: Model): List[Item] =
    m.all_items filter (_.kind == Comment)

  def flatten(m: Model, is: List[Item]): List[Item] =
    flatten_(children(m), is)

  def flatten_[T](f: T => List[T], is: List[T]): List[T] =
    is match {
      case Nil => Nil
      case i :: is => i :: flatten_(f, f(i)) ::: flatten_(f, is)
    }

  def concat_comments(m: Model, is: List[Item]): List[Item] =
    ((is map children(m)) :\ List[Item]())(_ ::: _)

  def metadata(i: Item): String =
    apply(i, List(("internal_id", show(_.internalId)),
                  ("parent",      show(_.parent)),
                  ("title",       _.title),
                  ("tags",        show_no_quotes(_.tags)),
                  ("permatitle",  _.permatitle),
                  ("kind",        show(_.kind)),
                  ("uid",         _.uid),
                  ("created",     show(_.created)),
                  ("updated",     show(_.updated)),
                  ("author",      show(_.author)),
                  ("visible",     show(_.visible)))
    ) mkString "\n"

  // curring
  def show(f: Item => Any)(i: Item): String =
    f(i) toString

  def show_no_quotes(f: Item => List[String])(i: Item): String =
    f(i) mkString ", "

  def apply(i: Item, xs: List[(String, Item => String)]): List[String] =
    xs match {
      case Nil => Nil
      case x :: xs => (x._1 + ": " + x._2(i)) :: apply(i, xs)
    }

  def body_block(i: Item): String =
    "--- START BODY ---\n" + i.body + "\n--- END BODY ---\n"

  def summary_block(i: Item): String =
    i.summary match {
      case None => ""
      case Some(x) => "--- START SUMMARY ---\n" + x + "\n--- END SUMMARY ---\n"
    }

  val default_author = Author(C.author_name, C.author_uri, C.author_email, true)
}

Comments

No comments.