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.