NetBeans(6.8) plugin for Scala 2.8.0 RC1
I'm please to announce the NetBeans (6.8) plugin for Scala 2.8.0 RC1 is available now. It's a maintain release to catch up with Scala 2.8.0 RC1.
The packed plugins is downloadable at: http://sourceforge.net/projects/erlybird/files/nb-scala/6.8v1.1.0rc2/
It requires Scala 2.8.0 RC1, which is available at: http://www.scala-lang.org/downloads. For maven project, it's under http://www.scala-tools.org/repo-releases, with version number "2.8.0.RC1"
Please refer to http://wiki.netbeans.org/Scala68v1 for the installation/upgrading information.
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) }
Progress of Migrating AIOTrade to Scala #2
My next step is to push AIOTrade to another level of UE, with hotkey for symbols, indicators etc, with better UI.
So far, my Scala adventure went almost smoothly, I'm thinking about the data exchange between client and data source, maybe the actors from Scala, LiftWeb or AKKA will bring some fresh atmosphere.
Progress of Migrating AIOTrade to Scala
Well, I've done most parts of migrating AIOTrade to Scala, not all features return yet. I gain lots of experiences of inter-op between Scala and Java, since AIOTrade has to be integrated into an existed Java framework NetBeans Platform. And also, whole project is now managed by Maven instead of Ant, which reduces lots of pain of dependencies upon crossing sub-projects.
This project is now hosted on kenai.com http://sf.net/projects/humaitrader, you can check out the code to get an overview of how to integrated Maven + Scala + NetBeans Modules. Of course, all were done with NetBeans Scala plugin.
LOC of this project so far:
$ ./cloc.pl --read-lang-def=lang_defs.txt ~/myprjs/aiotrade.kn/opensource/ 677 text files. 617 unique files. 154 files ignored. http://cloc.sourceforge.net v 1.08 T=3.0 s (167.7 files/s, 21373.7 lines/s) ------------------------------------------------------------------------------- Language files blank comment code scale 3rd gen. equiv ------------------------------------------------------------------------------- Scala 353 7981 16301 27180 x 1.36 = 36964.80 Java 43 1148 833 6946 x 1.36 = 9446.56 XML 104 231 389 2414 x 1.90 = 4586.60 Bourne Shell 2 81 81 488 x 3.81 = 1859.28 HTML 1 7 15 26 x 1.90 = 49.40 ------------------------------------------------------------------------------- SUM: 503 9448 17619 37054 x 1.43 = 52906.64 -------------------------------------------------------------------------------
A screen snapshot:
Jetty + FastCGI Servlet + Trac = blogtrader.net
I ported jfastcgi to Scala and fixed some bugs. Now my web site is configured running under Jetty with the modified fastcgi servlet as gateway to a Trac fastcgi daemon.
I encountered an issue (fixed later) with Trac fcgi daemon though, which may cause the daemon died silently, but the fastcgi servlet will restart it automatically when this happens.
All source code can be found via Browse Source
Importing Old Blogs Almost Done
I've almost done importing old blogs from Roller db to this Trac based one, except image/resource links.
The pretty url of this web site is also working now.
New Web Site for blogtrader.net
I just re-launched blogtrader.net, which is now based on trac, but on glassfish V3.0 (moved to Jetty 7.0 later) via a custom CGIServlet. It's a headache to get a pretty url for trac, I may re-explore it later.
All old blogs should be imported back here, but not now.
About Caoyuan's Blog
Caoyuan's blogs about future.
For more information about this site see Wiki
How to Setup Dependencies Aware Maven Project for Scala
I have to say, maintain a couple of dependent projects via Ant is a headache, just like me and others may have encountered when use NetBeans' Scala plugin to create and maintain plain Scala projects, these projects, are all Ant based. It's difficult to write a generic Ant template to compute the dependencies graph and then choose the best building path. The building process of cross dependent projects becomes very slow because the redundant enter/exit dependent projects building task.
NetBeans has also the best Maven plugin integrated, I decided to give Maven a trying. This was actually my first attempt to create Maven based project(s). I explored the mini way toward a Scala Maven project, and patched Scalac a bit again to get the dependencies aware compiling working.
Now here's an example I'd like to share with you.
Assume we have n sub-projects, with cross dependencies, for instance, 'lib.util' and 'lib.math', 'lib.indicators' etc. The simplest Maven way is to keep a parent project (I call it 'modules' here) which holds all common settings and module(sub-project) names(paths). The directory structure could be like:
modules |-- pom.xml |-- lib.util | |-- pom.xml | `-- src | |-- main | | `-- scala | | `-- lib | | `-- util | | `-- App.scala | `-- test | `-- scala | `-- lib | `-- util | `-- AppTest.scala |-- lib.math |-- pom.xml `-- src |-- main | `-- scala | `-- lib | `-- math | `-- App.scala `-- test `-- scala `-- lib `-- math `-- AppTest.scala
What I like Maven here is that the parent project 'modules' only maintains the common settings and module paths, all dependencies between sub-projects are then set in sub-project's self pom.xml file. This is a good decoupled strategy in my eyes, you can freely combine these sub-projects at any time without change too much, and each sub-project doesn't need to care about the dependencies of other projects. The parent project is only a centre place for sharing common setting and a centre place to list all available projects and their directories, it's just like a Directory Service.
Now, here is the pom.xml of parent project looks like (all common settings include compiler, repository etc are kept in it):
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.aiotrade</groupId> <artifactId>modules</artifactId> <version>1.0-SNAPSHOT</version> <packaging>pom</packaging> <name>AIOTrade Modules</name> <description>Basic Modules for AIOTrade.</description> <properties> <scala.version>2.8.0-SNAPSHOT</scala.version> </properties> <repositories> <repository> <id>scala-tools.release</id> <name>Scala-Tools Release Repository</name> <url>http://scala-tools.org/repo-releases</url> </repository> <repository> <id>scala-tools.snapshot</id> <name>Scala-Tools Snapshot Repository</name> <url>http://scala-tools.org/repo-snapshots</url> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>scala-tools.org</id> <name>Scala-Tools Maven2 Repository</name> <url>http://scala-tools.org/repo-releases</url> </pluginRepository> </pluginRepositories> <dependencies> <dependency> <groupId>org.scala-lang</groupId> <artifactId>scala-library</artifactId> <version>${scala.version}</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.4</version> <scope>test</scope> </dependency> <dependency> <groupId>org.specs</groupId> <artifactId>specs</artifactId> <version>1.2.5</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.scala-tools</groupId> <artifactId>maven-scala-plugin</artifactId> <executions> <execution> <phase>process-resources</phase> <!-- to support mix java/scala only --> <goals> <goal>add-source</goal> <!-- to support mix java/scala only --> <goal>compile</goal> <goal>testCompile</goal> </goals> </execution> </executions> <configuration> <jvmArgs> <jvmArg>-Xms64m</jvmArg> <jvmArg>-Xmx1024m</jvmArg> </jvmArgs> <scalaVersion>${scala.version}</scalaVersion> <args> <arg>-target:jvm-1.5</arg> <arg>-make:transitivenocp</arg> <arg>-dependencyfile</arg> <arg>${project.build.directory}/.scala_dependencies</arg> </args> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.5</source> <target>1.5</target> </configuration> </plugin> </plugins> </build> <reporting> <plugins> <plugin> <groupId>org.scala-tools</groupId> <artifactId>maven-scala-plugin</artifactId> <configuration> <scalaVersion>${scala.version}</scalaVersion> </configuration> </plugin> </plugins> </reporting> <modules> <module>lib.math</module> <module>lib.util</module> <module>lib.indicator</module> </modules> </project>
And the pom.xml of sub-project 'lib.util' looks like:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.aiotrade</groupId> <artifactId>modules</artifactId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>lib-util</artifactId> <version>1.0-SNAPSHOT</version> <name>lib-util</name> </project>
The 'lib.math' one:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.aiotrade</groupId> <artifactId>modules</artifactId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>lib-math</artifactId> <version>1.0-SNAPSHOT</version> <name>lib-math</name> <dependencies> <dependency> <groupId>${project.groupId}</groupId> <artifactId>lib.util</artifactId> <version>${project.version}</version> <type>jar</type> </dependency> </dependencies> </project>
That's all the settings, clear and clean.
Now, let's back to another level of dependencies: dependencies between Scala source files. How does this setting get scalac aware dependencies of changed sources?
Again, just like the scalac setting in an Ant based project, where at 'configuration' part of build/plugins/plugin/@maven-scala-plugin, I add "-make:transitivenocp -dependencyfile ${project.build.directory}/.scala_dependencies" to 'args' (should be put in separate 'arg' tag as following)
<configuration> <jvmArgs> <jvmArg>-Xms64m</jvmArg> <jvmArg>-Xmx1024m</jvmArg> </jvmArgs> <scalaVersion>${scala.version}</scalaVersion> <args> <arg>-target:jvm-1.5</arg> <arg>-make:transitivenocp</arg> <arg>-dependencyfile</arg> <arg>${project.build.directory}/.scala_dependencies</arg> </args> </configuration>
Now the last question, how to use NetBeans to create above setting?
Open "File" | "New Project..." | "Maven" | "Maven Project", click "Next", choose "Maven Quickstart Archetype" to create each empty project, then copy/paste above content to corresponding pom.xml, change project groupId, artifactId and version properties. For existed project, copy your source code to "main/scala". You can also mixed Java code, put then at "main/java", NetBeans Scala plugin supports mixed Scala/Java project well.
I'll put the pre-defined archetype on net later, so no need to copy/paste, or, you can create an empty parent/sub-project, commit to your version control system. BTW, I find manually modify pom.xml is a pleasure work.
- Note No. 1: You have to use/update-to Scala-2.8.0-SNAPHOST with at least scala-compiler-2.8.0-20091128-xxx.jar
- Note No. 2: If your project is simple, just use NetBeans created Ant based project. If you separate it to a couple of dependent projects, I strongly suggest you to Maven.
- Note No. 3: A new Scala plugin for NetBeans 6.8RC1 will be available before or at Monday, it's also a must upgrade especially for Windows users.
How to Setup Dependencies Aware Ant Project for Scala
During the past days, I was patching scalac ant task and some relative issues, and now, the dependencies aware ant scalac works (post Scala 2.8.0.r19724).
Below is an example build.xml with dependencies aware setting:
1 <?xml version="1.0" encoding="UTF-8"?> 2 <project name="ScalaAntTest" default="build" basedir="."> 3 <description>Builds, tests, and runs the project ScalaAntTest.</description> 4 5 <property name="src.dir" value="${basedir}/src"/> 6 <property name="build.dir" value="${basedir}/build"/> 7 <property name="build.classes.dir" value="${build.dir}/classes"/> 8 9 <target name="init"> 10 <property environment="env"/> 11 <condition property="scala.home" value="${env.SCALA_HOME}"> 12 <isset property="env.SCALA_HOME"/> 13 </condition> 14 <fail unless="scala.home">set SCALA_HOME first</fail> 15 16 <property name="scala-library.jar" value="${scala.home}/lib/scala-library.jar"/> 17 <property name="scala-compiler.jar" value="${scala.home}/lib/scala-compiler.jar"/> 18 19 <path id="build.classpath"> 20 <pathelement location="${scala-library.jar}"/> 21 <pathelement location="${scala-compiler.jar}"/> 22 <pathelement location="${build.classes.dir}"/> 23 </path> 24 <taskdef resource="scala/tools/ant/antlib.xml"> 25 <classpath> 26 <pathelement location="${scala-compiler.jar}"/> 27 <pathelement location="${scala-library.jar}"/> 28 </classpath> 29 </taskdef> 30 </target> 31 32 <target name="build" depends="init"> 33 <mkdir dir="${build.dir}"/> 34 <mkdir dir="${build.classes.dir}"/> 35 <scalac srcdir="${src.dir}" 36 destdir="${build.classes.dir}" 37 classpathref="build.classpath" 38 force="yes" 39 addparams="-make:transitive -dependencyfile ${build.dir}/.scala_dependencies" 40 > 41 <src path="${basedir}/src1"/> 42 <!--include name="compile/**/*.scala"/--> 43 <!--exclude name="forget/**/*.scala"/--> 44 </scalac> 45 </target> 46 47 <target name="clean" depends="init"> 48 <delete dir="${build.dir}"/> 49 </target> 50 51 </project>
There are some tips here, I'll give a concise explanation:
First, there will be a file call ".scala_dependencies" which is put under "build/" directory after you first clean-build, it will record all dependencies information. Since it's put under "build/", it will be removed automatically after an "ant clean". The "-dependencyfile ${build.dir}/.scala_dependencies" parameter of scalac at line 39 enables this.
Second, you should add "-make:transitive" as scalac's parameter (line 39), which will enable scalac to evaluate the dependencies transitive.
Third, add attribute "force='yes'" (line 38), which tells scalac to check all source files for dependencies and re-compile them if files that dependents on changed.
Forth, you should include "<pathelement location='${build.dir.classes}'>" as part of "build.classpath" (line 22), so scalac won't complain lack of already generated classes when rebuild upon parts of source files.
I've also re-write the project scheme that created by NetBeans plugin, that is, the next released NetBeans Scala plugin will automatically generate dependencies aware build.xml file for new created projects. Unfortunately, you'll have to copy/move your old project src to new created project directory if you want to benefit from it.
For some reasons, "fsc" is no longer supported as the default compiler task in NetBeans created project, which actually brought some annoyances for plugin users. Even without "fsc", the dependencies aware "scalac" should also work satisfiable in most cases.
Scala Plugin for NetBeans - Available for NetBeans 6.8beta and Scala 2.8.0 Snapshot
I packed another release of Scala plugin for NetBeans 6.8 beta, for more information, please see:
http://wiki.netbeans.org/Scala68v1
This version allows adding "deprecation", "unchecked" parameters for scalac, and fixed unicode id issue. It works on Scala-2.8.0 snapshot only.
Note:
All projects created via "File" -> "New Project" before this version should add or change following lines in "nbproject/project.properties":
scalac.compilerargs= scalac.deprecation=no scalac.unchecked=no
Scala Plugin for NetBeans - Available for NetBeans 6.8m2 and Scala 2.8.0 Snapshot
As NetBeans 6.8 m2 released, I packed a downloadable binary which works with Scala 2.8.0.r18993 or above.
Please do not install Scala plugin via Update Center for NetBeans 6.8 m2, it's not compatible. Use the link below to download and install
New & Noteworthy
- Much more responsive when typing and for code-completion
- More refactoring: find usages, rename across opened projects
- Breakpoint works everywhere (almost)
- Better supporting for mixed Java/Scala project in both direction (Java is visible in Scala and vice versa)
- Better integration with NetBeans maven plugin
- Better code completion even for implicit methods (in most cases)
- Better indentation and formatter for unfinished line after 'if', 'else', '=', 'for', 'while', 'do' etc
- Better syntax highlighting for val/var, lazy val, implicit call, byname param, abstract method etc
- Mark of implemented/overridden methods, click on mark will jump to super definition
- Go to type ("Ctrl+O")
- Select parts of code, press '{', '[', '(', '"', '`' will add/replace surrounding pair, press '~' will remove them. Specially, press '/' will block-comment it
- Reset Scala interactive parser when necessary, for instance: dependent libs changed (Right click on source, choose "Reset Scala Parser" in pop-up menu)
- Output highlighted code to html ([File] -> [Print to HTML...])
- Some basic hints, for instance: fixing import, unused imports
- Code template now works, check or add your own via [Options/Preferences] -> [Editor] -> [Code Templates] -> [Scala]
Install with NetBeans 6.8 M2
- Download and install the latest Scala-2.8.0 snapshot runtime via Scala's home
- Set $SCALA_HOME environment variable to point to the installed Scala runtime path. Add $SCALA_HOME/bin to PATH environment variable. Note for Mac OS user, $SCALA_HOME environment variable may not be visible for Applications/NetBeans, see http://wiki.netbeans.org/MacOSXEnvForApp
- Get the NetBeans 6.8 M2 from: http://bits.netbeans.org/netbeans/6.8/m2/
- Get the Scala plugins binary from: https://sourceforge.net/projects/erlybird/files/nb-scala/6.8v1.1.0m2/nb-scala-6.8v1.1.0m2.zip/download
- Unzip Scala plugin binary to somewhere
- Open NetBeans, go to "Tools" -> "Plugins", click on "Downloaded" tab title, click on "Add Plugins..." button, choose the directory where the Scala plugins are unzipped, select all listed *.nbm files, following the instructions.
Tips
- If you encounter "... Could not connect to compilation daemon.", try to run "fsc -reset" under a command/terminal window.
- Editor $NetBeansInstallationPath?/etc/netbeans.conf, remove "-ea" to avoid AssertionError? popped up
http://wiki.netbeans.org/Scala68v1 for more information
Notice for Host Migration of This Site
I'm going to migrate this web site to new host during this month, both blogtrader.net and aiotrade.com may not be available at any time during this period.
Scala Plugin for NetBeans - Rewrite in Scala #8: Partly Visible to Java and Go to Type
>>> Updated on Sep 8:
Now in most cases, Scala is visible to Java, including generic type. Mixed Java/Scala project should be well supported.
======
I need "Go To Type" working, and Scala's symbols should be visible to Java sources. For NetBeans, that means I should implement a Scala to Java VirtualSourceProvider?, which will translate Scala source to Java stub.
I tried it before, but not so successful, so I disabled it. Finally, I got how it works today, and committed work in part. That is, now, in Scala/Java mixed project, not only Java is visible to Scala source, and also, partly, Scala is visible to Java too.
Another benefit is, when you press "Ctrl + O" or "Command + O", a navigator window will bring you to Type's source file that you are searching.
I'll go on to get Scala -> Java mapping fully works soon.
>>> Updated on Sep 8:
The following issue was fixed in trunk.
======
Warning: If you have not got a NetBeans nightly built before, do not try it until a recent (after Sep 3rd) serious fault fixed.
Scala Plugin for NetBeans - Rewrite in Scala #7: Mark Override Method and Go to Super def
The new progress is a little mark at the left side bar of editor, showing if a method is overriding a method of base class/trait. Move cursor on this mark will show a tooltip, and, click on this mark will jump to source of super definition.
If a mark of (D) shown and you forget to add "override", you will wait until a whole build process to tell you lacking of "override", so pay attention to it.
Of course, if it's an (I) mark, you do not need to add "override".
Scala Plugin for NetBeans - Rewrite in Scala #6: Refactoring Step Two - Rename
As I've done most code of Refactoring skeleton, it did not cost me too much time to get renaming feature working (2 hours about). Now, NetBeans' Scala plugin can rename class/method/val etc across project's source files.
Following snapshot shows class "Dog" is going to be renamed to "BigDog?". After the preview, press "Do Refactoring", all things done.
Scala Plugin for NetBeans - Rewrite in Scala #4: How to Use It to Develop Scala Itself
I begin to patch Scala's new designed compiler interface for IDE to get it work better (with NetBeans, and, other IDEs). I know lamp term may use Eclipse as their IDE to develop Scala itself, but I'm using new written NetBeans plugin. Here's a short tutorial of how to use NetBeans Scala plugin to work with Scala itself
Build Scala-2.8.0 snapshot
First and least, svn check out Scala's trunk source code:
cd ~/myprjs/scala/scala svn co http://lampsvn.epfl.ch/svn-repos/scala/scala/trunk
Then, make sure your $JAVA_HOME is pointed to a JDK 1.5 instead of 1.6, and <b>clear $SCALA_HOME</b>. "ant dist" to build a fresh Scala distribution, which is located at ~myprjs/scala/scala/dist/latest
# As for Mac OS X JAVA_HOME_BAK=$JAVA_HOME export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/1.5.0/Home export SCALA_HOME= ant dist export JAVA_HOME=$JAVA_HOME_BAK export SCALA_HOME=~myprjs/scala/scala/dist/latest
Install latest NetBeans and Scala plugin
Download latest NetBeans nightly built from http://bits.netbeans.org/download/trunk/nightly/latest, the minimal pack "Java SE" is enough.
Run NetBeans, get latest Scala plugin via: [Preference/Option] -> [Plugins] -> [Available Plugins], find "Scala Kit" in the list and choose it,following the instructions to get it installed, restart NetBeans.
Create NetBeans project by importing existing sources
Create project for Scala's trunk sources. <b>Note: each folder under "src" should be considered as standalone folder, for example, "compiler"</b>, it's better to create standalone project for each these folder, I created two, one for "compiler",another for "library".
[File] -> [New Project] -> [Scala] -> [Scala Project with Existing Sources]
Click [Next], input project name that you like Note: you can input an existed ant build script file name in "Build Script Name". Or, modify the auto-created one later, it's a plain ant build xml file.
Click [Next], add source folder:
Click [Next] again, you can see the sources that will be included in this project, then click [Finish]
Now, Scala's compiler has been imported as a NetBeans freeform project, it is an old plain ant project. You can create another one for Scala's library.
How to debug into Scala's compiler, library source?
If you have a project that want to debug into Scala's compiler, library sources, you could do when in debugging:
In debugging, open debugging source windows via: [Windows] -> [Debugging] -> [Sources]. Go through the listed source jars, find ".../scala-compiler.jar" and ".../scala-library.jar", check them.
Scala Plugin for NetBeans - Rewrite in Scala #3: Ready for Beta Test
I struggled with new redesigned IDE / compiler interface (interative.Global) during this weekend, and finally, got it attached to NetBeans Scala plugin, which runs as a background daemon thread, thus the plugin is more responsive when coding (the first benefit). It was a hard work though, I had to modify some of the original code to get whole things stable and responsive (balance), it paid me 2 sleepless nights.
So here's what's new on current nightly built:
- Tested and work with Scala 2.8.0.r18542 snapshot
- Better supporting for mixed Java/Scala project
- Better indentation and formatter for unfinished line after 'if', 'else', '=', 'for', 'while', 'do' etc
- Better code completion even for implicit methods (some times)
- Implicit method call will be highlighted by underline
- Select parts of code, press '{', '[', '(', '"', '`' etc will add/replace surrounding pair, press '~' will remove them. Specially, press '/' will block comment it
- Some basic hints, for example, fix import (from Milos' work)
- Code template now works, check or add your own via [Options/Preferences] -> [Editor] -> [Code Templates] -> [Scala] (from Milos' work)
Note: wait until Monday night for NetBeans' hudson builds those whole new nbms.
I think this plugin is qualifiable for beta using/testing now. If you are interested in testing/reporting bugs, you can get the latest NetBeans nightly built, and got the new Scala plugin from Update Center, download a Scala-2.8.0 snapshot, live your $SCALA_HOME to this Scala 2.8.0 snapshot, change $PATH to including $SCALA_HOME/bin.
By testing/using Scala 2.8.0 right now, we can also push it improved more quickly.
BTW, I'm using this plugin on whole Scala' trunk source and of course, this plugin.
Scala Plugin for NetBeans - Rewrite in Scala #2: Supports Java/Scala Mixed Project
Java/Scala mixed project was supported partly once before, but I cannot remember when this feature got lost. Anyway, as one progressing of the rewritten NetBeans Scala plugin, I just got this feature working properly halfway: now Java source is visible to Scala one, but not vice versa. To got Scala source also visible for Java source, would pay for efficiency, since the compiler may need to be pushed to a deep phase, and mapped to Java classes/methods, I need more time to think about that.
Below is a screenshot that a JavaDog? is called from a Scala source file:
Scala Plugin for NetBeans - Rewrite in Scala #1: Almost Done
The previous Scala plugin for NetBeans was a rush work which was written in Java, toward a useful tool to support writing Scala code. I called it chicken, which was then can be used to produce new Scala plugin written in Scala, that was, the egg.
The egg has grown to a chicken now. As for last night, I switched whole scala modules to depend on the new written scala.editor module (in Scala), and use this new chicken to improve Scala plugin from now on.
The new scala plugin will be carefully rewritten, with a lot of APIs supporting for language editor in mind, and try to support better and more features toward a whole featured, stable Scala IDE.
Another good news is, Milos Kleint, the contributor to NetBeans Maven plugin, has also put hands on this project, he's working on better Scala support for maven project, and new templates/hinds features for Scala editor modules.
The new plugin is based on Scala 2.8 snapshot, if want to get it, you should use the nightly built NetBeans from netbeans.org, and get the new plugins via updated center.
Note: you have to install the latest Scala 2.8 snapshot too, and make sure $SCALA_HOME, $PATH pointed to it. For maven project, you should also update your pom.xml to depend on Scala -2.8-snapshot.
Below is a working snapshot that I was using new Scala plugin to write Scala plugin:
Scala Plugin for NetBeans: Scala 2.8 Snapshot
>>> Updated
Don't forget to download a fresh Scala 2.8.0-snapshot from scala-lang.org (you may want to track the progress of 2.8.0 once a week etc), and set SCALA_HOME, PATH environment to point to the installation of it.
======
During the weekend, I built a fresh Scala 2.8.0 snapshot. With ticket #2163 fixed in 18 hours (great job, Scala team), I got a Scala plugin for NetBeans that works under Scala 2.8.0. I then re-built whole Erlang plugin (which is written in Scala), by fixing some minor incompatible issues, it works too.
So, at least, Scala 2.8 seems stable enough for writing a NetBeans plugin now.
Here's a snapshot showing some new features of 2.8.0
If you are eager to have some taste of Scala 2.8.0 with NetBeans plugin, and is patient to keep another NetBeans nightly version on your machine, then, wait for one more day from now for NetBeans hudson to build whole things, download the lasted NetBeans nightly version, and install "Scala Kit" module via "Update Center".
Don't forget to download a fresh Scala 2.8.0-snapshot (you may want to track the progress of 2.8.0) from scala-lang.org, and maintain your SCALA_HOME, and PATH environment for different Scala versions
BTW, if I have time, I'll begin to rewrite this plugin in Scala, on 2.8.0
Rats! Plugin for NetBeans#1: Syntax Highlighting
I've used Rats! parser generator heavily on Scala/Erlang plugin for NetBeans, but not write a plugin for Rats! itself.
So I spent my weekend days on a simple Rats! editor module, which implemented syntax highlighting. It's built on Scala.
Here's the snapshot:
It will be available in couple of days.
Erlang Plugin Version 1 for NetBeans 6.7 Released
I'm pleased to announce Erlang plugin (ErlyBird) version 1 for NetBeans 6.7 is released.
NetBeans 6.7 RC3 or above is a requirement.
What's new:
- It's rewritten in Scala instead of Java
- More reliable instant rename
- Display extracted document information from source comment when doing auto-completion
To download, please go to: https://sourceforge.net/project/showfiles.php?group_id=192439&package_id=226387
To install:
- Open NetBeans, go to "Tools" -> "Plugins", click on "Downloaded" tab title, click on "Add Plugins..." button, choose the directory where the Erlang plugin are unzipped, select all listed *.nbm files, following the instructions.
- Make sure your Erlang bin path is under OS environment PATH, you can also check/set your OTP path: From [Tools]->[Erlang Platform], fill in the full path of your 'erl.exe' or 'erl' file in "Interpreter", for instance: "C:/erl/bin/erl.exe". Or open the "Brows" dialog to locate the erlang installation.
<li>When you open/create an Erlang project first time, the OTP libs will be indexed. Take a coffee and wait, the indexing time varies from 10 to 30 minutes depending on your computer.
Feedback and bug reports are welcome.
Scala Plugin Version 1 for NetBeans 6.7 Released
>>> Updated on July 1, 2009
There are couple of reports on "Could not connect to compilation daemon.", mostly under Windows OS. It's a known issue, to resolve it, please check the following:
- SCALA_HOME is set to a fresh installed Scala runtime
- PATH includes $SCALA_HOME/bin
- If the build task still complain, try to run "fsc" or "scala" in a command window first
======
I'm pleased to announce the availability of Scala plugin version 1 for NetBeans 6.7
What's new:
- Use fsc instead of scalac as the project building compiler (If you've set SCALA_HOME, make sure $SCALA_HOME/bin is included in your PATH environment).
- Fixed setting breakpoint in closure statement.
- A basic import-fixer (Right click on source, then choose "Fix Imports").
- Code assistant for local vars and functions.
- Run/Debug single file.
To download, please go to: https://sourceforge.net/project/showfiles.php?group_id=192439&package_id=256544&release_id=686747
For more information, please see http://wiki.netbeans.org/Scala
Bug reports are welcome.
It works on NetBeans 6.7 RC1 or above.
AIOTrade, Scala for NetBeans, NLP
For past years, I worked on AIOTrade project, which uses Machine Learning technology to find the structural data pattern.
Then, to find a better programming language for AIOTrade, I tried Erlang, Scala, and wrote IDEs for these candidate languages, I learnt a lot of Formal Language Processing.
Now, finally, I dove into Natural Language Processing, and have a good chance to composite the knowledge of Statistics Learning, Rule based Language Processing on massive structural/un-structural data under parallel computing environment.
The candidate programming language is Scala, I keep to improve the NetBeans support for Scala these days while reading the books/code of NLP. During the long weekend, I've got some improvements on it, here is a summary:
- Migrated to NetBeans' CSL
- Use fsc instead of scalac as the project building compiler
- Fixed setting breakpoint in closure statement
- A basic import-fixer
- Code assistant for local vars and functions
- Run/Debug single file
The plugins can be got for beta testing on NetBeans Developing Update Center (for NetBeans 6.7 beta+), and will be released around the date of official NetBeans 6.7.
AIOTrade: Progress of Migrating to Scala - DSL and Parallel Computing
Well, I've migrated the core math, indicator parts of AIOTrade to Scala. There are two noticeable advances for new AIOTrade.
The first is the look and feel of indicator writing. Since Scala is suitable for DSL, now the indicator looks like:
class ARBRIndicator extends ContIndicator { _sname = "AR/BR" _grids = Array(50f, 200f) val period = Factor("Period", 10) val up = Var[Float]("up") val dn = Var[Float]("dn") val bs = Var[Float]("bs") val ss = Var[Float]("ss") val ar = Var[Float]("AR", Plot.Line) val br = Var[Float]("BR", Plot.Line) def computeCont(begIdx:Int, size:Int) { for (i <- begIdx until size) { up(i) = H(i) - O(i) val up_sum_i = sum(i, up, period) dn(i) = O(i) - L(i) val dn_sum_i = sum(i, dn, period) ar(i) = up_sum_i / dn_sum_i * 100 val bs_tmp = H(i) - C(i) bs(i) = Math.max(0, bs_tmp) val bs_sum_i = sum(i, bs, period) val ss_tmp = C(i) - L(i) ss(i) = Math.max(0, ss_tmp) val ss_sum_i = sum(i, ss, period) br(i) = bs_sum_i / ss_sum_i * 100 } } }
Vs Java one:
public class ARBRIndicator extends ContIndicator { _sname = "AR/BR"; _grids = new Float[] {50f, 200f}; Opt period = new DefaultOpt("Period", 10); Var<Float> up = new DefaultVar("up"); Var<Float> dn = new DefaultVar("dn"); Var<Float> bs = new DefaultVar("bs"); Var<Float> ss = new DefaultVar("ss"); Var<Float> ar = new DefaultVar("AR", Plot.Line); Var<Float> br = new DefaultVar("BR", Plot.Line); void computeCont(int begIdx) { for (int i = begIdx; i < _itemSize; i++) { up.set(i, H.get(i) - O.get(i)); float up_sum_i = sum(i, up, period); dn.set(i, O.get(i) - L.get(i)); float dn_sum_i = sum(i, dn, period); ar.set(i, up_sum_i / dn_sum_i * 100); float bs_tmp = H.get(i) - C.get(i); bs.set(i, Math.max(0, bs_tmp)); float bs_sum_i = sum(i, bs, period); float ss_tmp = C.get(i) - L.get(i); ss.set(i, Math.max(0, ss_tmp)); float ss_sum_i = sum(i, ss, period); br.set(i, bs_sum_i / ss_sum_i * 100); } } }
The apply method from Scala simplifies setter/getter, which makes the formulator looks more natural.
The second is by implementing each indicator as Actor, the computing procedure of indicators can be easily distributed to multiple CPU cores, with so few code modification:
case object Compute trait Computable extends Actor { // ----- actor's implementation def act = loop { react { case (Compute, fromTime:Long) => computeFrom(fromTime) case _ => } } // ----- end of actor's implementation def computeFrom(time:Long) :Unit def computedTime :Long def factors :ArrayBuffer[Factor] def factors_=(factors:ArrayBuffer[Factor]) :Unit def factors_=(values:Array[Number]) :Unit def dispose :Unit }
Computable is an Interface/Trait with sync method: computeFrom(Long), now by extending Computable with Actor, implementing a simple function act with react message processing block, all indicators (which extended Computable) can benefit from parallel computing now by calling:
indicator ! (Compute, fromTime)
instead of
indicator.computeFrom(time)
I've done some testing on my 4-core machine, which occupied about 380% CPU usage during running. This is, of course, a most easily implementation for parallel computing under JVM so far.
Another bonus is, I do not need to worry about concurrent calling on computeFrom(Long) now, since all calls will be triggered by Compute messages that are sent to actor's message queue, then be processed sequentially, there is no lock needed any more. The key:
Parallel computing actors + Sequential message driven computing per actor
AIOTrade Is Migrating to Scala
Finally, after evaluated Erlang, Scala etc, wrote IDE tools for these languages, I began to migrate AIOTrade from Java to Scala, with the help of Scala for NetBeans of course.
The first step is re-writting basic modules to Scala smoothly, with little functional style code; Then, I'll re-design the APIs by Scala's advanced features, including function, trait, actors etc.
Since AIOTrade is a NetBeans suite project, with several NetBeans style modules integrated, I need a general purpose build.xml to get Scala based modules working. Here are build.xml and scala-build.xml, which can be used to write Scala based NetBeans platform modules.
First, you should create a regular NetBeans module project, then put/replace these ant files under your project's base director. You also need to create 2 NetBeans lib wrapper modules, one is for scala-library.jar, another is for scala-compile.jar as parts of your NetBeans suite project (or, check them out from AIOTrade's source repository)
build.xml:
<?xml version="1.0" encoding="UTF-8"?> <project name="lib.math" default="netbeans" basedir="."> <import file="scala-build.xml"/> </project>
scala-build.xml:
<?xml version="1.0" encoding="UTF-8"?> <project name="scala-module" default="netbeans" basedir="."> <import file="nbproject/build-impl.xml"/> <target name="scala-taskdef" depends="init"> <property name="scala.library" value="${cluster}/modules/ext/scala-library.jar"/> <property name="scala.compiler" value="${cluster}/modules/ext/scala-compiler.jar"/> <property name="scala.libs" value="${scala.library}:${scala.compiler}"/> <echo message="cluster: ${cluster}"/> <echo message="Compiling scala sources via ${scala.library}, ${scala.compiler}"/> <taskdef resource="scala/tools/ant/antlib.xml"> <classpath> <pathelement location="${cluster}/modules/ext/scala-library.jar"/> <pathelement location="${cluster}/modules/ext/scala-compiler.jar"/> </classpath> </taskdef> </target> <property name="jar-excludes" value="**/*.java,**/*.form,**/package.html,**/doc-files/,**/*.scala"/> <target name="compile" depends="init,up-to-date,scala-taskdef" unless="is.jar.uptodate"> <!-- javac's classpath should include scala.library and all these paths of "cp" --> <path id="javac.cp"> <pathelement path="${scala.libs}"/> <pathelement path="${module.classpath}"/> <pathelement path="${cp.extra}"/> </path> <!-- scalac will check class dependencies deeply, so we can not rely on public package only which is refed by ${module.classpath} --> <path id="scalac.cp"> <pathelement path="${scala.libs}"/> <pathelement path="${module.run.classpath}"/> <pathelement path="${cp.extra}"/> </path> <mkdir dir="${build.classes.dir}"/> <depend srcdir="${src.dir}" destdir="${build.classes.dir}" cache="build/depcache"> <classpath refid="scalac.cp"/> </depend> <!-- scalac --> <scalac srcdir="${src.dir}" destdir="${build.classes.dir}" encoding="UTF-8" target="jvm-${javac.target}"> <classpath refid="scalac.cp"/> </scalac> <!-- javac --> <nb-javac srcdir="${src.dir}" destdir="${build.classes.dir}" debug="${build.compiler.debug}" debuglevel="${build.compiler.debuglevel}" encoding="UTF-8" deprecation="${build.compiler.deprecation}" optimize="${build.compiler.optimize}" source="${javac.source}" target="${javac.target}" includeantruntime="false"> <classpath refid="javac.cp"/> <compilerarg line="${javac.compilerargs}"/> <processorpath refid="processor.cp"/> </nb-javac> <!-- Sanity check: --> <pathconvert pathsep=":" property="class.files.in.src"> <path> <fileset dir="${src.dir}"> <include name="**/*.class"/> </fileset> </path> </pathconvert> <fail> <condition> <not> <equals arg1="${class.files.in.src}" arg2=""/> </not> </condition> You have stray *.class files in ${src.dir} which you must remove. Probably you failed to clean your sources before updating them. </fail> <!-- OK, continue: --> <copy todir="${build.classes.dir}"> <fileset dir="${src.dir}" excludes="${jar-excludes}"/> </copy> </target> <target name="do-test-build" depends="projectized-common.do-test-build"> <scalac srcdir="${test.unit.src.dir}" destdir="${build.test.unit.classes.dir}" excludes="${test.excludes}" encoding="UTF-8"> <classpath refid="test.unit.cp"/> </scalac> </target> </project>
BTW, the new source code of AIOTrade is controlled under Mercurial version control system on sourceforge.net, you can clone or brows the code at: AIOTrade source repository
Note: The whole project can not be successfully built yet.
AIOTrade Applet Demo Fixed Timezone Issue
I fixed AIOTrade timezone issue. And, the applet demo on aiotrade.com does not need to be a signed applet any more.
For recent experience, I'm re-considering the RIA platform choice. May applet be saved in the near future? or Flash, Javascript win? First, Java applet needs a whole new look and feel theme, which make it fitting into the web page, second, how can a Java applet access DOM tree easily?
But,
When Flash/Flex tries to take the place of Java applet, it will become another Java itself.
New Face of aiotrade.com with AIOTrade Applet Demo
I've done some preliminary re-design on http://aiotrade.com, put the AIOTrade applet on home page, it requires browser with Java plugin above JRE 1.5, the size is about 288k. You can input the symbol to get real-time quote charting. Symbol is same as Yahoo! finance. The quote data is also from Yahoo! finance.
There are still some bugs on it. I'll continue to improve it.
Here's the snapshot of whole home page after you input a symbol. You can remove the chart by press "Remove Quotes" button.
AIOTrade - It's not Flash or A Picture It's A Java Applet on Web Page
For RIA framework, there are choices of Flex/Flash, Applet/JavaFX etc. I saw a lot of real-time financial charts were written in Flex/Flash, all over the world wide web. Then, how about a Java applet financial charting after Java 6u10? I'd like a try, and here is the result:
- The whole size of a most featured AIOTrade is less than 300k
- On my Macbook, with Java 6u12 installed, the init time is about 1 sec.
- By properly arranging Swing layout, the applet can auto-scale to proper size fit in html page just like any element.
Below is a snapshot:
Erlang Plugin for NetBeans in Scala#11: Indexer
For IDE support, an indexer is very important for declaration-find, code-completion, re-factory etc. Indexer will monitor the standard lib and your project, analyzing the source/binary files, gather meta-information of classes, functions, global vars, usages, and store them in a search engine (It's a Lucene back-end in NetBeans). That means, you need to support project management and platform management first, so you know where's the class/source path of platform and your current opened projects.
ErlyBird implemented Erlang platform and project management in modules: erlang.platform and erlang.project, these code are derived from Tor's Ruby module, I've migrated them to CSL, but did not rewrote them in Scala. It's OK for integrating these Java written modules with Scala written erlang.editor module.
First, you should identify the class path type, you can register them in ErlangLanguage.scala as:
/** @see org.netbeans.modules.erlang.platform.ErlangPlatformClassPathProvider and ModuleInstall */ override def getLibraryPathIds = Collections.singleton(BOOT) override def getSourcePathIds = Collections.singleton(SOURCE) object ErlangLanguage { val BOOT = "erlang/classpath/boot" val COMPILE = "erlang/classpath/compile" val EXECUTE = "erlang/classpath/execute" val SOURCE = "erlang/classpath/source" }
Where, BOOT, thus getLibraryPathIds will point to Erlang OTP libs' path later in:
org.netbeans.modules.erlang.platform.ErlangPlatformClassPathProvider
Now, the indexer itself, ErlangIndex.scala which extends CSL's EmbeddingIndexer and implemented:
override protected def index(indexable:Indexable, parserResult:Result, context:Context) :Unit
with a factory:
class Factory extends EmbeddingIndexerFactory
Register it in ErlangLanguage.scala as:
override def getIndexerFactory = new ErlangIndexer.Factory
Now, tell the CSL to monitor and index them, for Erlang OTP libs, this is done in ModuleInstall.java:
@Override public void restored() { GlobalPathRegistry.getDefault().register(ErlangPlatformClassPathProvider.BOOT, new ClassPath[] { ErlangPlatformClassPathProvider.getBootClassPath() }); } @Override public void uninstalled() { GlobalPathRegistry.getDefault().unregister(ErlangPlatformClassPathProvider.BOOT, new ClassPath[] { ErlangPlatformClassPathProvider.getBootClassPath() }); }
All classpaths registered in GlobalPathRegistry will be monitored and indexed automatically. As "ModuleInstall.java" is also registered in module erlang.platform's manifest.mf as OpenIDE-Module-Install class, it will be automatically loaded and invoked when this module is installed/activated in NetBeans:
Manifest-Version: 1.0 OpenIDE-Module: org.netbeans.modules.erlang.platform OpenIDE-Module-Layer: org/netbeans/modules/erlang/platform/resources/layer.xml OpenIDE-Module-Localizing-Bundle: org/netbeans/modules/erlang/platform/Bundle.properties OpenIDE-Module-Install: org/netbeans/modules/erlang/platform/ModuleInstall.class OpenIDE-Module-Specification-Version: 0.18.0
For project's classpath, the "GlobalPathRegistry registering is at RubyProject.java (don't be confused by the name, it's a derivation from Ruby's code, I did not change it yet), as:
private final class ProjectOpenedHookImpl extends ProjectOpenedHook { ProjectOpenedHookImpl() {} protected void projectOpened() { // register project's classpaths to GlobalPathRegistry ClassPathProviderImpl cpProvider = lookup.lookup(ClassPathProviderImpl.class); GlobalPathRegistry.getDefault().register(RubyProject.BOOT, cpProvider.getProjectClassPaths(RubyProject.BOOT)); GlobalPathRegistry.getDefault().register(RubyProject.SOURCE, cpProvider.getProjectClassPaths(RubyProject.SOURCE)); } protected void projectClosed() { // unregister project's classpaths to GlobalPathRegistry ClassPathProviderImpl cpProvider = lookup.lookup(ClassPathProviderImpl.class); GlobalPathRegistry.getDefault().unregister(RubyProject.SOURCE, cpProvider.getProjectClassPaths(RubyProject.SOURCE)); } }
So the project's source path will be registered when this project is opened, thus, trigger the index engine.
Now, when you first install Erlang plugin, IDE will index whole OTP libs once. And when an Erlang project is opened, the project's source files will be indexed too.
Then, you need to do a reverse job: search index, get the meta-data to composite Erlang symbols and AST items. This is done in ErlangIndex.scala, which has helper methods to search the index fields, and fetch back the stored signature string, extract it to symbol, and invoke the parsing manager to resolve the AST items. With the help of ErlangIndex.scala, you can search across the whole libs, find completion items, find declarations, find documents embedded in source comments, find usages.
As the first use of index feature, I've implemented the code-completion and go-to-declaration for global modules functions:
When user input "lists:", will invoke code-completion feature, since it's a remote function call, code-completion will search the index data, and find it's from OTP's "lists.erl", after parsing this file, we get all exported functions AstDfn, so, there are a lot of meta-information can be used now, I'll implement the documents tooltips later.
This will be the final one of the series of Erlang plug-in in Scala blogs, thanks for reading and feedback. I'll take a break during the weekend. From the next Monday, with the spring is coming, I'll join a new project, which is an ambitious financial information platform, I'll bring Scala, Lift, Erlang to it. BTW, congratulations to Lift 1.0!
Erlang Plugin for NetBeans in Scala#10: Code Completion
Implementing Code-Completion is a bit complex, but you can got it work gradually. At the first step, you can implement Code-Completion for local vars/functions only, then, with the indexed supporting, you can add completion for remote functions.
You should define some kinds of completion proposal, which may show different behaviors when they are popped up and guard you followed steps. For example, a function proposal can auto-fill parameters, on the other side, a keyword proposal just complete itself.
The completion proposal classes are defined in ErlangComplectionProposal.scala, which implemented CSL's interface CompletionProposal. you may notice that the function proposal is the most complex one, which should handle parameters information.
Then, you should implement CSL's interface CodeCompletionHandler, for Erlang, it's ErlangCodeCompletion, where, the key method is:
override def complete(context:CodeCompletionContext) :CodeCompletionResult = { this.caseSensitive = context.isCaseSensitive val pResult = context.getParserResult.asInstanceOf[ErlangParserResult] val lexOffset = context.getCaretOffset val prefix = context.getPrefix match { case null => "" case x => x } val kind = if (context.isPrefixMatch) QuerySupport.Kind.PREFIX else QuerySupport.Kind.EXACT val queryType = context.getQueryType val doc = LexUtil.document(pResult, true) match { case None => return CodeCompletionResult.NONE case Some(x) => x.asInstanceOf[BaseDocument] } val proposals = new ArrayList[CompletionProposal] val completionResult = new DefaultCompletionResult(proposals, false) // Read-lock due to Token hierarchy use doc.readLock try { val astOffset = LexUtil.astOffset(pResult, lexOffset) if (astOffset == -1) { return CodeCompletionResult.NONE } val root = pResult.rootScope match { case None => return CodeCompletionResult.NONE case Some(x) => x } val th = LexUtil.tokenHierarchy(pResult).get val fileObject = LexUtil.fileObject(pResult).get val request = new CompletionRequest request.completionResult = completionResult request.result = pResult request.lexOffset = lexOffset request.astOffset = astOffset request.index = ErlangIndex.get(pResult) request.doc = doc request.info = pResult request.prefix = prefix request.th = th request.kind = kind request.queryType = queryType request.fileObject = fileObject request.anchor = lexOffset - prefix.length request.root = root ErlangCodeCompletion.request = request val token = LexUtil.token(doc, lexOffset - 1) match { case None => return completionResult case Some(x) => x } token.id match { case ErlangTokenId.LineComment => // TODO - Complete symbols in comments? return completionResult case ErlangTokenId.StringLiteral => //completeStrings(proposals, request) return completionResult case _ => } val ts = LexUtil.tokenSequence(th, lexOffset - 1) match { case None => return completionResult case Some(x) => x.move(lexOffset - 1) if (!x.moveNext && !x.movePrevious) { return completionResult } x } val closetToken = LexUtil.findPreviousNonWsNonComment(ts) if (root != null) { val sanitizedRange = pResult.sanitizedRange val offset = if (sanitizedRange != OffsetRange.NONE && sanitizedRange.containsInclusive(astOffset)) { sanitizedRange.getStart } else astOffset val call = Call(null, null, false) findCall(root, ts, th, call, 0) val prefixBak = request.prefix call match { case Call(null, _, _) => case Call(base, _, false) => // it's not a call, but may be candicate for module name, try to get modules and go-on completeModules(base, proposals, request) case Call(base, select, true) => if (select != null) { request.prefix = call.select.text.toString } else { request.prefix = "" } completeModuleFunctions(call.base, proposals, request) // Since is after a ":", we won't added other proposals, just return now whatever return completionResult } request.prefix = prefixBak completeLocals(proposals, request) } completeKeywords(proposals, request) } finally { doc.readUnlock } completionResult }
For a Erlang function call, you should check the tokens surrounding the caret to get the call's base name and select first, which is done by a method findCall:
private def findCall(rootScope:AstRootScope, ts:TokenSequence[TokenId], th:TokenHierarchy[_], call:Call, times:Int) :Unit = { assert(rootScope != null) val closest = LexUtil.findPreviousNonWsNonComment(ts) val idToken = closest.id match { case ErlangTokenId.Colon => call.caretAfterColon = true // skip RParen if it's the previous if (ts.movePrevious) { val prev = LexUtil.findPreviousNonWs(ts) if (prev != null) { prev.id match { case ErlangTokenId.RParen => LexUtil.skipPair(ts, ErlangTokenId.LParen, ErlangTokenId.RParen, true) case ErlangTokenId.RBrace => LexUtil.skipPair(ts, ErlangTokenId.LBrace, ErlangTokenId.RBrace, true) case ErlangTokenId.RBracket => LexUtil.skipPair(ts, ErlangTokenId.LBracket, ErlangTokenId.RBracket, true) case _ => } } } LexUtil.findPrevIncluding(ts, LexUtil.CALL_IDs) case id if LexUtil.CALL_IDs.contains(id) => closest case _ => null } if (idToken != null) { times match { case 0 if call.caretAfterColon => call.base = idToken case 0 if ts.movePrevious => LexUtil.findPreviousNonWsNonComment(ts) match { case null => call.base = idToken case prev if prev.id == ErlangTokenId.Colon => call.caretAfterColon = true call.select = idToken findCall(rootScope, ts, th, call, times + 1) case _ => call.base = idToken } case _ => call.base = idToken } } } case class Call(var base:Token[TokenId], var select:Token[TokenId], var caretAfterColon:Boolean)
To complete a remote function call, you may need to visit outer modules, which needs an indexer, so as the first step, you can just ignore it, go straight to complete local vars/functions, or keywords:
private def completeLocals(proposals:List[CompletionProposal], request:CompletionRequest) :Unit = { val prefix = request.prefix val kind = request.kind val pResult = request.result val root = request.root val closestScope = root.closestScope(request.th, request.astOffset) match { case None => return case Some(x) => x } val localVars = closestScope.visibleDfns(ElementKind.VARIABLE) localVars ++= closestScope.visibleDfns(ElementKind.PARAMETER) localVars.filter{v => filterKind(kind, prefix, v.name)}.foreach{v => proposals.add(new PlainProposal(v, request.anchor)) } val localFuns = closestScope.visibleDfns(ElementKind.METHOD) localFuns.filter{f => filterKind(kind, prefix, f.name)}.foreach{f => proposals.add(new FunctionProposal(f, request.anchor)) } } private def completeKeywords(proposals:List[CompletionProposal], request:CompletionRequest) :Unit = { val prefix = request.prefix val itr = LexerErlang.ERLANG_KEYWORDS.iterator while (itr.hasNext) { val keyword = itr.next if (startsWith(keyword, prefix)) { proposals.add(new KeywordProposal(keyword, null, request.anchor)) } } }
There is a function def visibleDfns(kind: ElementKind): ArrayBuffer[AstDfn] in AstRootScope.scala, if you've put definition items properly in scopes, it should handle the visibility automatically.
Now, register it in ErlangLanguage.scala:
override def getCompletionHandler = new ErlangCodeCompletion
As usual, run it, you got:
The local functions and vars are proposed plus the keywords. BTW, I've fixed this feature for Scala plug-in.
Scala Corner Case#3: "object" or "case object" Extends Case Class? It's a Different Story
Case class in Scala is a neat feature, it automatically generates apply, extract and equals functions. But I encountered a strange "equals" behavior today, which wasted me 2 hours to find why.
Define a simple case class "Symbol", then a "VarSymbol" which extends "Symbol" with one more field "name:String". We know you can compare the equality of instances of "VarSymbol" by this "name", Scala automatically gets it right. And of course, a direct instance from "Symbol()" should not equal to any "VarSymbol", since it lacks "name" field, that's right.
case class Symbol() case class VarSymbol(name:String) extends Symbol
Then, I need a singleton object "NoSymbol", I defined it as:
object NoSymbol extends Symbol
I of course thought this "NoSymbol" should not equal any "VarSymbol" instance, or,
NoSymbol == VarSymbol("I'm var")
Should return false. But life is not so straightforward, it returns true in real life !!!
I finally got my code working, by adding a "case" before "object NoSymbol extends Symbol". This "NoSymbol" won't equal any "VarSymbol" now, I'm grad things finally go back in track.
I don't know if it's a Scala bug, or, that's what Scala thinks it should be: all "Symbol" and it's inherited instances should equal "NoSymbol" object. Anyway, here's the whole code for test:
case class Symbol() case class VarSymbol(name:String) extends Symbol object NoSymbol extends Symbol case object CasedNoSymbol extends Symbol object TestCaseObject { def test = { val noSym = NoSymbol val caseNoSym = CasedNoSymbol val varSym = VarSymbol("I'm var") if (noSym == varSym) println("NoSym equals varSym !") else println("NoSym doesn't equal varSym") if (caseNoSym == varSym) println("CaseNoSym equals varSym !") else println("CaseNoSym doesn't equal varSym") } }
Run TestCaseObject.test, I got:
NoSym equals varSym ! CaseNoSym doesn't equal varSym
Better Look&Feel of NetBeans on Mac OS
NetBeans 6.7M2 is going to be public available soon, the new look&feel on Mac OS is very awesome. BTW, -Dapple.awt.graphics.UseQuartz=true is a must option for best font-rendering on my macbook. I added it to netbeans.conf as -J-Dapple.awt.graphics.UseQuartz=true. Here is a snapshot of my current screen when working on Erlang plugin in Scala:
Click on the picture to enlarge it:
Scala Corner Case#2: "Nothing" Can not Be Cast and Assigned to a val/var
There is a Java class which takes type parameter T, T is any type:
package dcaoyuan.test; import java.util.ArrayList; import java.util.Iterator; import java.util.List; public class AList<T> implements Iterator<T> { List<T> a = new ArrayList<T>(); { a.add((T) "string"); } Iterator<T> itr = a.iterator(); public boolean hasNext() { return itr.hasNext(); } public T next() { return itr.next(); } public void remove() { throw new UnsupportedOperationException("Not supported yet."); } }
Which implemented java.util.Iterator with type parametered T next(). Notice here next() will return T
The above code simulates a case, that an existed Java lib may return an instance that has type parameter erased, on run-time.
Now let's try to call it in Scala:
package dcaoyuan.test object Main { def main(args: Array[String]) :Unit = { testAList_work testAList_unwork } def testAList_work :Unit = { val a :AList[_] = new AList // a is inferred as AList[Any] while (a.hasNext) { val e = a.next println(e) } } def testAList_unwork :Unit = { val a = new AList // a is inferred as AList[Nothing] while (a.hasNext) { // now a.next is Nothing, can not cast to "val e", which should at least be Any val e = a.next // will throw java.lang.ClassCastException println(e) } } }
I wrote two functions: "testAList_work" and "testAList_unwork"
The issue is in testAList_unwork, as a simple "val a = new AList", "a" will be inferred by scalac as AList[Nothing], so, "a.next" will return an instance of type Nothing, then, "val e = a.next" will throw java.lang.ClassCastException
My question is, should this corner case be checked by scalac as an error or warning? So I do not need to dig the clause when it happens on run-time.
Scala Corner Case#1: Implement Method of Java Interface with Type Parameter Omitted by It's Sub-Class
The progress of rewriting Erlang plugin for NetBeans in Scala has reached a phase, that the Editor itself works smooth and better than ErlyBird now, the next step is to integrate an Erlang project management and index the modules/functions of OTP/project to provide smart auto-completion.
Now Scala has been proved that it can be integrated into an existed large Java based framework (NetBeans IDE here) without no much problems, I'll begin to rewrite Scala plugin in Scala soon.
Although in most cases, Scala can call existed Java code or classes smoothly, there are still some corner cases. I'll record these corner cases in blogs. Here's the first one which I also posted on scala-user mailing-list, but have not yet got final answer.
Let's begin with a detailed example:
There is a Java interface A:
public interface A<T extends String> { void run(T t); }
Which has a type parameter <T extends String> and abstract method run(T t)
Then a Java abstract class B extended A. But, B, as it, omitted type parameter from A. This is unsafe but valid in Java:
public abstract class B implements A { public String me() { return "I'm B"; } }
Assume above classes A and B have been compiled under javac, and packed in a jar library, and I can not patch it anymore. Now I need to write a class S in Scala which should extend B:
class S extends B { override def run[T <: String](t:T) = {println(t)} }
scalac will complain as:
/Users/dcaoyuan/NetBeansProjects/ScalaTestCase/src/S.scala:1: error: class S needs to be abstract, since method run in trait A of type (T)Unit is not defined class S extends B { /Users/dcaoyuan/NetBeansProjects/ScalaTestCase/src/S.scala:3: error: method run overrides nothing def run[T <: String](t:T) = {println(t)}
I than tried "forSome" type:
class S extends B { override def run(t:T forSome {type T <: String}) = {println(t)} }
The code still did not work.
It seems that, since B omitted A's type parameter T, I have no way to get what is T, and can not successfully implement "run(T t)" method.
I also tried other forms of "forSome" usages, and always failed.
But I think Scala can always be saved with mixed Java/Scala code in such corner case, that's what I believed. So, I thought about it later when my brain was spare, and finally got a solution:
I wrote another Java abstract class B1 which extends B and pretended to have implemented "run(T t)", but actually called another new abstract method "runImpl(String t)"
public abstract class B1 extends B { public void run(String t) { runImpl(t); } public abstract void runImpl(String t); }
Now I can let Scala S extends B1 and implement "runImpl(String t)" instead of extending B and implementing "run(T t)".
class S extends B1 { override def runImpl(t:String) = {println(t)} }
Yes, scalac won't complain about "runImpl(t:String)" at all, and I got S successfully extends B by bridge class B1.
But I still hope scalac can resolve it directly, with a warning message instead of failing to compile it.
Erlang Plugin for NetBeans in Scala#9: Instant Rename and Go to Declaration
It seems I'm the only one who is committing to hg.netbeans.org/main/contrib these days. Anyway.
Implementing Instant Rename and Go to Declaration is not difficult with AstDfn/AstRef/AstScope. Here is the code:
- Instant Rename is implemented by ErlangInstantRenamer.scala
- Go to Declaration is implemented by ErlangDeclarationFinder.scala
override def getInstantRenamer = new ErlangInstantRenamer override def getDeclarationFinder = new ErlangDeclarationFinder
Only local function definitions can be traveled to at this time. To jump to remote function definitions, I have to implement an indexer first.
Erlang Plugin for NetBeans in Scala#8: Pretty Formatting and Pair Matching
Now let's go on the complex part: Pretty Formatting and Pair Matching. I say they are complex, not because these features are much heavier on language's semantic complex. Implementing these features mostly deals with lexer tokens. But it's a bit brain-dried work to across forward/backward in the token stream to get the pair matching and pretty formatting working as you expected.
Because of the complex, I won't describe the details of how to implement them for Erlang, I just put the links to these source code:
- Pretty Formatting is implemented by ErlangFormatter.scala
- Pair Matching is implemented by ErlangKeystrokeHandler.scala
And registered them in ErlangLanguage.scala as:
override def getKeystrokeHandler = new ErlangKeystrokeHandler override def hasFormatter = true override def getFormatter = new ErlangFormatter
With these feature implemented, the new plugin can automatically complete/match braces and pair, indent properly when you hit BREAK, input a "end" etc.
BTW, the navigator window was improved during these days, it can now properly show the arity/args of each functions. It's done by improved AstNodeVisitor.scala and AstDfn.scala
Erlang Plugin for NetBeans in Scala#7: Occurrences Finder
During the AST node visiting (AstNodeVisitor.scala), I've gathered a lot of variable definitions and their references, we can now try to get editor to mark these occurrences. There are preliminary functions in AstScope.scala, such as findOccurrences(AstItem). You can override AstDfn#isReferredBy(AstRef) and AstRef#isOccurence(AstRef) to get accurate reference relations. As the first step, I just simply judge the reference relation by the equation of names.
We'll extends org.netbeans.modules.csl.api.OccurrencesFinder and implement:
run(pResult:ErlangParserResult, event:SchedulerEvent) :Unit
First, we try to get the AstItem which is at the caretPosition by rootScope.findItemAt(th, caretPosition), and verify if it's a valid token.
Then, by calling rootScope.findOccurrences(item), we collect all occurrences of this item, and put a ColoringAttributes.MARK_OCCURRENCES with its OffsetRange.
The code of ErlangOccurrencesFinder.scala:
/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common * Development and Distribution License("CDDL") (collectively, the * "License"). You may not use this file except in compliance with the * License. You can obtain a copy of the License at * http://www.netbeans.org/cddl-gplv2.html * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the * specific language governing permissions and limitations under the * License. When distributing the software, include this License Header * Notice in each file and include the License file at * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this * particular file as subject to the "Classpath" exception as provided * by Sun in the GPL Version 2 section of the License file that * accompanied this code. If applicable, add the following below the * License Header, with the fields enclosed by brackets [] replaced by * your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" * * If you wish your version of this file to be governed by only the CDDL * or only the GPL Version 2, indicate your decision by adding * "[Contributor] elects to include this software in this distribution * under the [CDDL or GPL Version 2] license." If you do not indicate a * single choice of license, a recipient has the option to distribute * your version of this file under either the CDDL, the GPL Version 2 or * to extend the choice of license to its licensees as provided above. * However, if you add GPL Version 2 code and therefore, elected the GPL * Version 2 license, then the option applies only if the new code is * made subject to such option by the copyright holder. * * Contributor(s): * * Portions Copyrighted 2009 Sun Microsystems, Inc. */ package org.netbeans.modules.erlang.editor import _root_.java.util.{HashMap,List,Map} import javax.swing.text.Document import org.netbeans.api.lexer.{Token,TokenId,TokenHierarchy} import _root_.org.netbeans.modules.csl.api.{ColoringAttributes,OccurrencesFinder,OffsetRange} import org.netbeans.modules.parsing.spi.Parser import org.netbeans.modules.parsing.spi.{Scheduler,SchedulerEvent} import org.netbeans.modules.erlang.editor.ast.{AstDfn,AstItem,AstRef,AstRootScope} import org.netbeans.modules.erlang.editor.lexer.{ErlangTokenId,LexUtil} import org.openide.filesystems.FileObject /** * * @author Caoyuan Deng */ class ErlangOccurrencesFinder extends OccurrencesFinder[ErlangParserResult] { protected var cancelled = false private var caretPosition = 0 private var occurrences :Map[OffsetRange, ColoringAttributes] = _ private var file :FileObject = _ protected def isCancelled = synchronized {cancelled} protected def resume :Unit = synchronized {cancelled = false} override def getPriority = 0 override def getSchedulerClass = Scheduler.CURSOR_SENSITIVE_TASK_SCHEDULER override def getOccurrences :Map[OffsetRange, ColoringAttributes] = occurrences override def cancel :Unit = synchronized {cancelled = true} override def setCaretPosition(position:Int) :Unit = {this.caretPosition = position} def run(pResult:ErlangParserResult, event:SchedulerEvent) :Unit = { resume if (pResult == null || isCancelled) { return } val currentFile = pResult.getSnapshot.getSource.getFileObject if (currentFile != file) { // Ensure that we don't reuse results from a different file occurrences = null file = currentFile } for (rootScope <- pResult.rootScope; th <- LexUtil.tokenHierarchy(pResult); doc <- LexUtil.document(pResult, true); // * we'll find item by offset of item's idToken, so, use caretPosition directly item <- rootScope.findItemAt(th, caretPosition); idToken <- item.idToken ) { var highlights = new HashMap[OffsetRange, ColoringAttributes](100) val astOffset = LexUtil.astOffset(pResult, caretPosition) if (astOffset == -1) { return } // * When we sanitize the line around the caret, occurrences // * highlighting can get really ugly val blankRange = pResult.sanitizedRange if (blankRange.containsInclusive(astOffset)) { return } // * test if document was just closed? LexUtil.document(pResult, true) match { case None => return case _ => } try { doc.readLock val length = doc.getLength val astRange = LexUtil.rangeOfToken(th.asInstanceOf[TokenHierarchy[TokenId]], idToken.asInstanceOf[Token[TokenId]]) val lexRange = LexUtil.lexerOffsets(pResult, astRange) var lexStartPos = lexRange.getStart var lexEndPos = lexRange.getEnd // If the buffer was just modified where a lot of text was deleted, // the parse tree positions could be pointing outside the valid range if (lexStartPos > length) { lexStartPos = length } if (lexEndPos > length) { lexEndPos = length } LexUtil.token(doc, caretPosition) match { case None => return case token => // valid token, go on } } finally { doc.readUnlock } val _occurrences = rootScope.findOccurrences(item) for (_item <- _occurrences; _idToken <- _item.idToken ) { highlights.put(LexUtil.rangeOfToken(th.asInstanceOf[TokenHierarchy[TokenId]], _idToken.asInstanceOf[Token[TokenId]]), ColoringAttributes.MARK_OCCURRENCES) } if (isCancelled) { return } if (highlights.size > 0) { val translated = new HashMap[OffsetRange, ColoringAttributes](2 * highlights.size) val entries = highlights.entrySet.iterator while (entries.hasNext) { val entry = entries.next LexUtil.lexerOffsets(pResult, entry.getKey) match { case OffsetRange.NONE => case range => translated.put(range, entry.getValue) } } highlights = translated this.occurrences = highlights } else { this.occurrences = null } } } }
Finally, again, register it in ErlangLanguage.scala
override def hasOccurrencesFinder = true override def getOccurrencesFinder = new ErlangOccurrencesFinder
And add mark-occurrences effect setting in fontsColors.xml:
<fontcolor name="mark-occurrences" bgColor="ECEBA3"/>
That's all. Run it, you got:
Var LogPid's definition and references were highlighted now.
The correctness of occurrences marking depends on the correctness of scopes of variables when you visit AST node, I did not finish all of such work yet, but, enabling occurrences marking feature is very helpful for further work.
Erlang Plugin for NetBeans in Scala#6: Semantic Analyzer
With more detailed AstNodeVisitor.scala, I got the semantic information of function calls, variable definitions and references etc. It's time to implement CSL's SemanticAnalyzer, which is the entrance of semantic highlighting.
I then encountered a Scala's corner case issue :-(
SemanticAnalyzer.java is a sub-class of ParserResultTask:
package org.netbeans.modules.parsing.spi; public abstract class ParserResultTask<T extends Parser.Result> extends SchedulerTask { public abstract void run (T result, SchedulerEvent event); public abstract int getPriority (); }
ParserResultTask's signature in class file is:
<<TLorg/netbeans/modules/parsing/spi/Parser$Result;>Lorg/netbeans/modules/parsing/spi/SchedulerTask;>
run's method signature:
<(TT;Lorg/netbeans/modules/parsing/spi/SchedulerEvent;)V>
ErlangSemanticAnalyzer extended SemanticAnalyzer, so I should implement:
void run (T result, SchedulerEvent event);
which carries type parameter T from ParserResultTask<T extends Parser.Result>. But unfortunately, SemanticAnalyzer extends ParserResultTask as:
public abstract class SemanticAnalyzer extends ParserResultTask;
That is, SemanticAnalyzer erased type parameter of its parent. It's valid in Java. But in Scala, I can not successfully extend SemanticAnalyzer anymore, since when I tried to implement "void run (T result, SchedulerEvent event)", the scalac always complained that I did not override this method with type T. It seems scalac checks type parameter deeply, and needs sub-class to always explicitly extend parent class with type parameter. Since SemanticAnalyzer has erased type parameter, even I tried to implement "run" as:
override def run[T <: Parser.Result](result:T, event:SchedulerEvent) :Unit
scalac still complained about it.
I have no idea of how to bypass this issue. I then patched SemanticAnalyzer.java, let it carries same type parameter as its parent:
public abstract class SemanticAnalyzer<T extends Parser.Result> extends ParserResultTask<T>
Now everything works. My final code is:
/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 1997-2008 Sun Microsystems, Inc. All rights reserved. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common * Development and Distribution License("CDDL") (collectively, the * "License"). You may not use this file except in compliance with the * License. You can obtain a copy of the License at * http://www.netbeans.org/cddl-gplv2.html * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the * specific language governing permissions and limitations under the * License. When distributing the software, include this License Header * Notice in each file and include the License file at * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this * particular file as subject to the "Classpath" exception as provided * by Sun in the GPL Version 2 section of the License file that * accompanied this code. If applicable, add the following below the * License Header, with the fields enclosed by brackets [] replaced by * your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" * * If you wish your version of this file to be governed by only the CDDL * or only the GPL Version 2, indicate your decision by adding * "[Contributor] elects to include this software in this distribution * under the [CDDL or GPL Version 2] license." If you do not indicate a * single choice of license, a recipient has the option to distribute * your version of this file under either the CDDL, the GPL Version 2 or * to extend the choice of license to its licensees as provided above. * However, if you add GPL Version 2 code and therefore, elected the GPL * Version 2 license, then the option applies only if the new code is * made subject to such option by the copyright holder. * * Contributor(s): * * Portions Copyrighted 2009 Sun Microsystems, Inc. */ package org.netbeans.modules.erlang.editor import _root_.java.util.{HashMap,HashSet,Map,Set} import javax.swing.text.Document import org.netbeans.api.lexer.Token import org.netbeans.api.lexer.TokenHierarchy import org.netbeans.api.lexer.TokenId import org.netbeans.modules.csl.api.{ColoringAttributes,ElementKind,OffsetRange,SemanticAnalyzer} import org.netbeans.modules.parsing.spi.Parser import org.netbeans.modules.parsing.spi.{Scheduler,SchedulerEvent,ParserResultTask} import org.netbeans.modules.erlang.editor.ast.{AstDfn,AstItem,AstRef,AstRootScope} import org.netbeans.modules.erlang.editor.lexer.LexUtil import org.netbeans.modules.erlang.editor.lexer.ErlangTokenId /** * * @author Caoyuan Deng */ class ErlangSemanticAnalyzer extends SemanticAnalyzer[ErlangParserResult] { private var cancelled = false private var semanticHighlights :Map[OffsetRange, Set[ColoringAttributes]] = _ protected def isCancelled :Boolean = synchronized {cancelled} protected def resume :Unit = synchronized {cancelled = false} override def getHighlights :Map[OffsetRange, Set[ColoringAttributes]] = semanticHighlights override def getPriority = 0 override def getSchedulerClass = Scheduler.EDITOR_SENSITIVE_TASK_SCHEDULER override def cancel :Unit = {cancelled = true} @throws(classOf[Exception]) override def run(pResult:ErlangParserResult, event:SchedulerEvent) :Unit = { resume semanticHighlights = null if (pResult == null || isCancelled) { return } for (rootScope <- pResult.rootScope; th <- LexUtil.tokenHierarchy(pResult); doc <- LexUtil.document(pResult, true) ) { var highlights = new HashMap[OffsetRange, Set[ColoringAttributes]](100) visitItems(th.asInstanceOf[TokenHierarchy[TokenId]], rootScope, highlights) this.semanticHighlights = if (highlights.size > 0) highlights else null } } private def visitItems(th:TokenHierarchy[TokenId], rootScope:AstRootScope, highlights:Map[OffsetRange, Set[ColoringAttributes]]) :Unit = { import ElementKind._ for (item <- rootScope.idTokenToItem(th).values; hiToken <- item.idToken ) { val hiRange = LexUtil.rangeOfToken(th, hiToken.asInstanceOf[Token[TokenId]]) item match { case dfn:AstDfn => dfn.getKind match { case MODULE => highlights.put(hiRange, ColoringAttributes.CLASS_SET) case CLASS => highlights.put(hiRange, ColoringAttributes.CLASS_SET) case ATTRIBUTE => highlights.put(hiRange, ColoringAttributes.STATIC_SET) case METHOD => highlights.put(hiRange, ColoringAttributes.METHOD_SET) case PARAMETER => highlights.put(hiRange, ColoringAttributes.PARAMETER_SET) case _ => } case ref:AstRef => ref.getKind match { case METHOD => highlights.put(hiRange, ColoringAttributes.FIELD_SET) case PARAMETER => highlights.put(hiRange, ColoringAttributes.PARAMETER_SET) case _ => } } } } }
In ErlangSemanticeAnalyzer, what you need to do is traversing all AstDfn and AstRef instances, and give them a proper ColoringAttributes set. I marked all functions as METHOD_SET(mod-method), attributes as STATIC_SET(mod-static), and function call names as FIELD_SET(mod-field), parameters as PARAMETER_SET(mod-parameter), the names in parenthesis are the corresponding names that in fontColors.xml setting file, you can define the highlighting effects in this xml file.
Again, don't forget to register it in ErlangLanguage.scala:
override def getSemanticAnalyzer = new ErlangSemanticAnalyzer
Run it, and I got highlighted source code as:
The function declaration names are underline, parameters are bold, function calls are bold too.
I'll gather even more detailed semantic information later, so I can identity the unused variable definitions and var references without definitions etc.
Erlang Plugin for NetBeans in Scala#5: Structure Analyzer
During the weekend, I've done some preliminary error recover work for Erlang's rats! definition. Now I'll go on some features based on analysis on AST tree. As the simplest step, we'll visit/analyze AST tree to get the structure information, use them for Navigator window and code folding.
First, we need to record the Structure information in some way. Each language has different AST tree and may be generated by varies tools, the AST element should be wrapped in some more generic classes to get integrated into CSL framework. There is an interface "org.netbeans.modules.csl.api.ElementKind", which is used for this purpose. But it's not enough, we need some facilities to not only wrap AST element, and also help us to identify the element as a definition or reference, in which visible scope etc.
I wrote a couple of these facilities when I wrote Erlang/Fortress/Scala plug-ins, these facilities can be repeatedly used for other languages by minor modifying. They are AstItem, AstDfn, AstRef, AstScope, AstRootScope. For Erlang plugin, you can find them under director: erlang.editor/src/org/netbeans/modules/erlang/editor/ast
AstDfn.scala is used to store a definition, such as definitions of class, module, method, variable, attribute etc. AstRef.scala is used to store reference that refers to definition, for example, the usages of a class, a variable, a method call etc.
All these AST items are stored in instances of AstScope.scala, which, is a container to identify the visible scope of references/definitions and store its sub-scopes. There are functions in AstScope to help to identify a variable's visible scope, find the definition of a reference, find references/occurrences of a definition etc.There should be some lexer level utilities too, to help you get the corresponding tokens when you visit AST tree. It's LexUtil.scala, which will be also heavy used for code-folding, indentation ... features.
With above facilities, we can visit an AST tree now, I'll do the simplest task first: get all functions/attribues name and bounds tokens.
AstNodeVisitor.scala
/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 2008 Sun Microsystems, Inc. All rights reserved. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common * Development and Distribution License("CDDL") (collectively, the * "License"). You may not use this file except in compliance with the * License. You can obtain a copy of the License at * http://www.netbeans.org/cddl-gplv2.html * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the * specific language governing permissions and limitations under the * License. When distributing the software, include this License Header * Notice in each file and include the License file at * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this * particular file as subject to the "Classpath" exception as provided * by Sun in the GPL Version 2 section of the License file that * accompanied this code. If applicable, add the following below the * License Header, with the fields enclosed by brackets [] replaced by * your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" * * If you wish your version of this file to be governed by only the CDDL * or only the GPL Version 2, indicate your decision by adding * "[Contributor] elects to include this software in this distribution * under the [CDDL or GPL Version 2] license." If you do not indicate a * single choice of license, a recipient has the option to distribute * your version of this file under either the CDDL, the GPL Version 2 or * to extend the choice of license to its licensees as provided above. * However, if you add GPL Version 2 code and therefore, elected the GPL * Version 2 license, then the option applies only if the new code is * made subject to such option by the copyright holder. * * Contributor(s): * * Portions Copyrighted 2009 Sun Microsystems, Inc. */ package org.netbeans.modules.erlang.editor.node import org.netbeans.api.lexer.{Token, TokenId, TokenHierarchy, TokenSequence} import org.netbeans.modules.csl.api.ElementKind import org.netbeans.modules.erlang.editor.ast.{AstDfn, AstItem, AstRef, AstRootScope, AstScope, AstVisitor} import org.netbeans.modules.erlang.editor.lexer.ErlangTokenId._ import org.netbeans.modules.erlang.editor.lexer.{ErlangTokenId, LexUtil} import org.openide.filesystems.FileObject import scala.collection.mutable.ArrayBuffer import xtc.tree.GNode import xtc.tree.Node import xtc.util.Pair /** * * @author Caoyuan Deng */ class AstNodeVisitor(rootNode:Node, th:TokenHierarchy[_], fo:FileObject) extends AstVisitor(rootNode, th) { def visitS(that:GNode) = { val formNodes = that.getList(0).iterator while(formNodes.hasNext) { visitForm(formNodes.next) } } def visitForm(that:GNode) = { enter(that) val scope = new AstScope(boundsTokens(that)) rootScope.addScope(scope) scopes.push(scope) visitNodeOnly(that.getGeneric(0)) exit(that) scopes.pop } def visitAttribute(that:GNode) = { that.get(0) match { case atomId:GNode => val attr = new AstDfn(that, idToken(idNode(atomId)), scopes.top, ElementKind.ATTRIBUTE, fo) rootScope.addDfn(attr) } } def visitFunction(that:GNode) = { visitFunctionClauses(that.getGeneric(0)) } def visitFunctionClauses(that:GNode) = { val fstClauseNode = that.getGeneric(0) visitFunctionClause(fstClauseNode) } def visitFunctionClause(that:GNode) = { val id = idNode(that.getGeneric(0)) val fun = new AstDfn(that, idToken(id), scopes.top, ElementKind.METHOD, fo) rootScope.addDfn(fun) } def visitRule(that:GNode) = { } }
As you can see, I just visit function/attribute related AST nodes, and gather all function/attribute declarations (or, definitions). My AST tree is generated by rats!, so the code looks like above. If you are using other parsers, I assume you know of course how to travel it.
AstNodeVisitor extends AstVisitor, I wrote some helper methods in AstVisitor.scala, for example, getting the bounds tokens for a definition/scope.
Now, you need to put the AST visiting task to ErlangParser.scala, so, it will be called when parsing finished, and you'll get an AST tree. The code is something like:
private def analyze(context:Context) :Unit = { val doc = LexUtil.document(context.snapshot, false) // * we need TokenHierarchy to do anaylzing task for (root <- context.root; th <- LexUtil.tokenHierarchy(context.snapshot)) { // * Due to Token hierarchy will be used in analyzing, should do it in an Read-lock atomic task for (x <- doc) {x.readLock} try { val visitor = new AstNodeVisitor(root, th, context.fo) visitor.visit(root) context.rootScope = Some(visitor.rootScope) } catch { case ex:Throwable => ex.printStackTrace } finally { for (x <- doc) {x.readUnlock} } } }
The rootScope will be carried in ErlangParserResult.scala.
With the visitor's rootScope, we can implement the navigator window and code folding now. What you need is to implement an org.netbeans.modules.csl.api.StructureScanner. My implementation is: ErlangStructureAnalyzer.scala
/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common * Development and Distribution License("CDDL") (collectively, the * "License"). You may not use this file except in compliance with the * License. You can obtain a copy of the License at * http://www.netbeans.org/cddl-gplv2.html * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the * specific language governing permissions and limitations under the * License. When distributing the software, include this License Header * Notice in each file and include the License file at * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this * particular file as subject to the "Classpath" exception as provided * by Sun in the GPL Version 2 section of the License file that * accompanied this code. If applicable, add the following below the * License Header, with the fields enclosed by brackets [] replaced by * your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" * * If you wish your version of this file to be governed by only the CDDL * or only the GPL Version 2, indicate your decision by adding * "[Contributor] elects to include this software in this distribution * under the [CDDL or GPL Version 2] license." If you do not indicate a * single choice of license, a recipient has the option to distribute * your version of this file under either the CDDL, the GPL Version 2 or * to extend the choice of license to its licensees as provided above. * However, if you add GPL Version 2 code and therefore, elected the GPL * Version 2 license, then the option applies only if the new code is * made subject to such option by the copyright holder. * * Contributor(s): * * Portions Copyrighted 2009 Sun Microsystems, Inc. */ package org.netbeans.modules.erlang.editor; import _root_.java.util.{ArrayList,Collections,HashMap,List,Map,Set,Stack} import javax.swing.ImageIcon; import javax.swing.text.{BadLocationException,Document} import org.netbeans.api.lexer.{Token,TokenId,TokenHierarchy,TokenSequence} import org.netbeans.editor.{BaseDocument,Utilities} import org.netbeans.modules.csl.api.{ElementHandle,ElementKind,HtmlFormatter,Modifier,OffsetRange,StructureItem,StructureScanner} import org.netbeans.modules.csl.api.StructureScanner._ import org.netbeans.modules.csl.spi.ParserResult import org.netbeans.modules.erlang.editor.ast.{AstDfn,AstRootScope,AstScope} import org.netbeans.modules.erlang.editor.lexer.{ErlangTokenId,LexUtil} import org.openide.util.Exceptions import scala.collection.mutable.ArrayBuffer /** * * @author Caoyuan Deng */ class ErlangStructureAnalyzer extends StructureScanner { override def getConfiguration :Configuration = null override def scan(result:ParserResult) :List[StructureItem] = result match { case null => Collections.emptyList[StructureItem] case pResult:ErlangParserResult => var items = Collections.emptyList[StructureItem] for (rootScope <- pResult.rootScope) { items = new ArrayList[StructureItem](rootScope.dfns.size) scanTopForms(rootScope, items, pResult) } items } private def scanTopForms(scope:AstScope, items:List[StructureItem], pResult:ErlangParserResult) :Unit = { for (dfn <- scope.dfns) { dfn.getKind match { case ElementKind.ATTRIBUTE | ElementKind.METHOD => items.add(new ErlangStructureItem(dfn, pResult)) case _ => } scanTopForms(dfn.bindingScope, items, pResult) } } override def folds(result:ParserResult) :Map[String, List[OffsetRange]] = result match { case null => Collections.emptyMap[String, List[OffsetRange]] case pResult:ErlangParserResult => var folds = Collections.emptyMap[String, List[OffsetRange]] for (rootScope <- pResult.rootScope; doc <- LexUtil.document(pResult, true); th <- LexUtil.tokenHierarchy(pResult); ts <- LexUtil.tokenSequence(th, 1) ) { folds = new HashMap[String, List[OffsetRange]] val codefolds = new ArrayList[OffsetRange] folds.put("codeblocks", codefolds); // NOI18N // * Read-lock due to Token hierarchy use doc.readLock addCodeFolds(pResult, doc, rootScope.dfns, codefolds) var lineCommentStart = 0 var lineCommentEnd = 0 var startLineCommentSet = false val comments = new Stack[Array[Integer]] val blocks = new Stack[Integer] while (ts.isValid && ts.moveNext) { val token = ts.token token.id match { case ErlangTokenId.LineComment => val offset = ts.offset if (!startLineCommentSet) { lineCommentStart = offset startLineCommentSet = true } lineCommentEnd = offset case ErlangTokenId.Case | ErlangTokenId.If | ErlangTokenId.Try | ErlangTokenId.Receive => val blockStart = ts.offset blocks.push(blockStart) startLineCommentSet = false case ErlangTokenId.End if !blocks.empty => val blockStart = blocks.pop.asInstanceOf[Int] val blockRange = new OffsetRange(blockStart, ts.offset + token.length) codefolds.add(blockRange) startLineCommentSet = false case _ => startLineCommentSet = false } } doc.readUnlock try { /** @see GsfFoldManager#addTree() for suitable fold names. */ lineCommentEnd = Utilities.getRowEnd(doc, lineCommentEnd) if (Utilities.getRowCount(doc, lineCommentStart, lineCommentEnd) > 1) { val lineCommentsFolds = new ArrayList[OffsetRange]; val range = new OffsetRange(lineCommentStart, lineCommentEnd) lineCommentsFolds.add(range) folds.put("comments", lineCommentsFolds) // NOI18N } } catch { case ex:BadLocationException => Exceptions.printStackTrace(ex) } } folds } @throws(classOf[BadLocationException]) private def addCodeFolds(pResult:ErlangParserResult, doc:BaseDocument, defs:ArrayBuffer[AstDfn], codeblocks:List[OffsetRange]) :Unit = { import ElementKind._ for (dfn <- defs) { val kind = dfn.getKind kind match { case FIELD | METHOD | CONSTRUCTOR | CLASS | MODULE | ATTRIBUTE => var range = dfn.getOffsetRange(pResult) var start = range.getStart // * start the fold at the end of the line behind last non-whitespace, should add 1 to start after "->" start = Utilities.getRowLastNonWhite(doc, start) + 1 val end = range.getEnd if (start != -1 && end != -1 && start < end && end <= doc.getLength) { range = new OffsetRange(start, end) codeblocks.add(range) } case _ => } val children = dfn.bindingScope.dfns addCodeFolds(pResult, doc, children, codeblocks) } } private class ErlangStructureItem(val dfn:AstDfn, pResult:ParserResult) extends StructureItem { import ElementKind._ override def getName :String = dfn.getName override def getSortText :String = getName override def getHtml(formatter:HtmlFormatter) :String = { dfn.htmlFormat(formatter) formatter.getText } override def getElementHandle :ElementHandle = dfn override def getKind :ElementKind = dfn.getKind override def getModifiers :Set[Modifier] = dfn.getModifiers override def isLeaf :Boolean = dfn.getKind match { case MODULE | CLASS => false case CONSTRUCTOR | METHOD | FIELD | VARIABLE | OTHER | PARAMETER | ATTRIBUTE => true case _ => true } override def getNestedItems : List[StructureItem] = { val nested = dfn.bindingScope.dfns if (nested.size > 0) { val children = new ArrayList[StructureItem](nested.size) for (child <- nested) { child.kind match { case PARAMETER | VARIABLE | OTHER => case _ => children.add(new ErlangStructureItem(child, pResult)) } } children } else Collections.emptyList[StructureItem] } override def getPosition :Long = { try { LexUtil.tokenHierarchy(pResult) match { case None => 0 case Some(th) => dfn.boundsOffset(th) } } catch {case ex:Exception => 0} } override def getEndPosition :Long = { try { LexUtil.tokenHierarchy(pResult) match { case None => 0 case Some(th) => dfn.boundsEndOffset(th) } } catch {case ex:Exception => 0} } override def equals(o:Any) :Boolean = o match { case null => false case x:ErlangStructureItem if dfn.getKind == x.dfn.getKind && getName.equals(x.getName) => true case _ => false } override def hashCode :Int = { var hash = 7 hash = (29 * hash) + (if (getName != null) getName.hashCode else 0) hash = (29 * hash) + (if (dfn.getKind != null) dfn.getKind.hashCode else 0) hash } override def toString = getName override def getCustomIcon :ImageIcon = null } }
Which just travels the rootScope, fetches AstDfn's instances and wrap them to ErlangStructureItem.
The last step is register this structure analyzer to ErlangLanguage.Scala as usual:
override def hasStructureScanner = true override def getStructureScanner = new ErlangStructureAnalyzer
For structure analyzer, you need also to register it in layer.xml:
<folder name="CslPlugins"> <folder name="text"> <folder name="x-erlang"> <file name="language.instance"> <attr name="instanceClass" stringvalue="org.netbeans.modules.erlang.editor.ErlangLanguage"/> </file> <file name="structure.instance"> <attr name="instanceClass" stringvalue="org.netbeans.modules.erlang.editor.ErlangStructureAnalyzer"/> </file> </folder> </folder> </folder>
Build and run, now open a .erl file, you got:
Click on the picture to enlarge it
The functions/attributes are shown in the left-side navigator window now, there are also some code-folding marks. Did you notice my preliminaty error-recover work? :-)
Erlang Plugin for NetBeans in Scala#4: Minimal Parser Intergation
With lexer integrated, you've got all tokens of source file. You can code for pretty formatting, indentation, auto pair inserting, pair matching based on tokens now. But I'll try to integrate an Erlang parser first to get the basic infrastructure ready.
I can choose to use Erlang's native compiler, than via jInterface, Scala can rpc call Erlang runtime to parse source file and return an AST tree. But, Erlang's library function for parsing is not good on error-recover currently, so I choose to write an Erlang parser in Rats!. Rats! generated parser is bad on error messages and error-recover too, but I know how to improve it.
Migrating a NetBeans Schliemann based grammar definition to Rats! is almost straightforward. For me, as I'm already familiar with Rats!, it's an one day work. The preliminary Rats! definition for Erlang can be found at ParserErlang.rats. Then, I rewrote a new ParserErlang.rats according to latest Erlang spec in another half day, without the LL(k) limitation, the grammar rules now keep as close as the original spec.
To get it be used to generate a ParserErlang.java, I added corresponding ant target to build.xml, which looks like:
<target name="rats" depends="init" description="Scanner"> <echo message="Rebuilding token scanner... ${rats.package.dir}"/> <java fork="yes" dir="${src.dir}/${rats.package.dir}" classname="xtc.parser.Rats" classpath="${rats.jar}"> <arg value="-in"/> <arg value="${src.dir}"/> <arg value="${rats.lexer.file}"/> </java> <echo message="Rebuilding grammar parser... ${rats.package.dir}"/> <java fork="yes" dir="${src.dir}/${rats.package.dir}" classname="xtc.parser.Rats" classpath="${rats.jar}"> <arg value="-in"/> <arg value="${src.dir}"/> <arg value="${rats.parser.file}"/> </java> </target>
And rats related properties, such as "rats.lexer.file" and "rats.parser.file" are defined in nbproject/project.properties as:
rats.jar=${cluster}/modules/xtc.jar rats.package.dir=org/netbeans/modules/erlang/editor/rats rats.lexer.file=LexerErlang.rats rats.parser.file=ParserErlang.rats
Running target "rats" will generate LexerErlang.java and ParserErlang.java, the first one is that we've used in ErlangLexer.scala, the later one is to be integrated into NetBeans' Parsing API as the parser.
Now, you should extend two Parsing API abstract classes, org.netbeans.modules.csl.spi.ParserResult and org.netbeans.modules.parsing.spi.Parser. The first one will carry result's AST Node and syntax errors that parser detected, the errors will be highlighted automatically in Editor. The second one is the bridge between NetBeans parsing task and your real parser (ParserErlang.java here)
There are tricks to do some error-recover in these two classes, by adding a "." or "end" or "}" etc to the buffered source chars when a syntax error occurred, it's called "sanitize" the source to recover the error. NetBeans' Ruby, JavaScripts supporting have some good examples of this trick. For my case, I will do error recover in Rats! definition later, so I do not use this trick currently, but I leave some "sanitize" related code there.
First, it's an ErlangParserResult.scala which extended ParserResult. The code is simple at the first phase.
ErlangParserResult.scala
/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common * Development and Distribution License("CDDL") (collectively, the * "License"). You may not use this file except in compliance with the * License. You can obtain a copy of the License at * http://www.netbeans.org/cddl-gplv2.html * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the * specific language governing permissions and limitations under the * License. When distributing the software, include this License Header * Notice in each file and include the License file at * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this * particular file as subject to the "Classpath" exception as provided * by Sun in the GPL Version 2 section of the License file that * accompanied this code. If applicable, add the following below the * License Header, with the fields enclosed by brackets [] replaced by * your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" * * Contributor(s): * * The Original Software is NetBeans. The Initial Developer of the Original * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun * Microsystems, Inc. All Rights Reserved. * * If you wish your version of this file to be governed by only the CDDL * or only the GPL Version 2, indicate your decision by adding * "[Contributor] elects to include this software in this distribution * under the [CDDL or GPL Version 2] license." If you do not indicate a * single choice of license, a recipient has the option to distribute * your version of this file under either the CDDL, the GPL Version 2 or * to extend the choice of license to its licensees as provided above. * However, if you add GPL Version 2 code and therefore, elected the GPL * Version 2 license, then the option applies only if the new code is * made subject to such option by the copyright holder. */ package org.netbeans.modules.erlang.editor import _root_.java.util.{Collections, ArrayList, List} import org.netbeans.api.lexer.{TokenHierarchy, TokenId} import org.netbeans.modules.csl.api.Error import org.netbeans.modules.csl.api.OffsetRange import org.netbeans.modules.csl.spi.ParserResult import org.netbeans.modules.parsing.api.Snapshot import org.netbeans.modules.erlang.editor.rats.ParserErlang import xtc.tree.{GNode} /** * * @author Caoyuan Deng */ class ErlangParserResult(parser:ErlangParser, snapshot:Snapshot, val rootNode:GNode, val th:TokenHierarchy[_]) extends ParserResult(snapshot) { override protected def invalidate :Unit = { // XXX: what exactly should we do here? } override def getDiagnostics :List[Error] = _errors private var _errors = Collections.emptyList[Error] def errors = _errors def errors_=(errors:List[Error]) = { this._errors = new ArrayList[Error](errors) } var source :String = _ /** * Return whether the source code for the parse result was "cleaned" * or "sanitized" (modified to reduce chance of parser errors) or not. * This method returns OffsetRange.NONE if the source was not sanitized, * otherwise returns the actual sanitized range. */ var sanitizedRange = OffsetRange.NONE var sanitizedContents :String = _ var sanitized :Sanitize = NONE var isCommentsAdded :Boolean = false /** * Set the range of source that was sanitized, if any. */ def setSanitized(sanitized:Sanitize, sanitizedRange:OffsetRange, sanitizedContents:String) :Unit = { this.sanitized = sanitized this.sanitizedRange = sanitizedRange this.sanitizedContents = sanitizedContents } override def toString = { "ErlangParseResult(file=" + snapshot.getSource.getFileObject + ",rootnode=" + rootNode + ")" } }
Then, the ErlangParser.scala which extended Parser:
ErlangParser.scala
/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common * Development and Distribution License("CDDL") (collectively, the * "License"). You may not use this file except in compliance with the * License. You can obtain a copy of the License at * http://www.netbeans.org/cddl-gplv2.html * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the * specific language governing permissions and limitations under the * License. When distributing the software, include this License Header * Notice in each file and include the License file at * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this * particular file as subject to the "Classpath" exception as provided * by Sun in the GPL Version 2 section of the License file that * accompanied this code. If applicable, add the following below the * License Header, with the fields enclosed by brackets [] replaced by * your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" * * Contributor(s): * * The Original Software is NetBeans. The Initial Developer of the Original * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun * Microsystems, Inc. All Rights Reserved. * * If you wish your version of this file to be governed by only the CDDL * or only the GPL Version 2, indicate your decision by adding * "[Contributor] elects to include this software in this distribution * under the [CDDL or GPL Version 2] license." If you do not indicate a * single choice of license, a recipient has the option to distribute * your version of this file under either the CDDL, the GPL Version 2 or * to extend the choice of license to its licensees as provided above. * However, if you add GPL Version 2 code and therefore, elected the GPL * Version 2 license, then the option applies only if the new code is * made subject to such option by the copyright holder. */ package org.netbeans.modules.erlang.editor import _root_.java.io.{IOException, StringReader} import _root_.java.util.ArrayList import _root_.java.util.Collection import _root_.java.util.List import _root_.java.util.ListIterator import _root_.javax.swing.event.ChangeListener import _root_.javax.swing.text.BadLocationException import org.netbeans.modules.csl.api.ElementHandle import org.netbeans.modules.csl.api.Error import org.netbeans.modules.csl.api.OffsetRange import org.netbeans.modules.csl.api.Severity import org.netbeans.modules.csl.spi.DefaultError import org.netbeans.modules.parsing.api.Snapshot import org.netbeans.modules.parsing.api.Task import org.netbeans.modules.parsing.spi.ParseException import org.netbeans.modules.csl.api.EditHistory import org.netbeans.modules.csl.spi.GsfUtilities import org.netbeans.modules.csl.spi.ParserResult import org.netbeans.modules.parsing.api.Source import org.netbeans.modules.parsing.spi.Parser import org.netbeans.modules.parsing.spi.Parser.Result import org.netbeans.modules.parsing.spi.ParserFactory import org.netbeans.modules.parsing.spi.SourceModificationEvent import org.openide.filesystems.FileObject import org.openide.util.Exceptions import org.netbeans.api.editor.EditorRegistry import org.netbeans.api.lexer.{TokenHierarchy, TokenId} import org.netbeans.editor.BaseDocument import org.netbeans.modules.editor.NbEditorUtilities import xtc.parser.{ParseError, SemanticValue} import xtc.tree.{GNode, Location} import org.netbeans.modules.erlang.editor.lexer.ErlangTokenId import org.netbeans.modules.erlang.editor.rats.ParserErlang /** * * @author Caoyuan Deng */ class ErlangParser extends Parser { private var lastResult :ErlangParserResult = _ @throws(classOf[ParseException]) override def parse(snapshot:Snapshot, task:Task, event:SourceModificationEvent) :Unit = { val context = new Context(snapshot, event) lastResult = parseBuffer(context, NONE) lastResult.errors = context.errors } @throws(classOf[ParseException]) override def getResult(task:Task) :Result = { assert(lastResult != null, "getResult() called prior parse()") //NOI18N lastResult } override def cancel :Unit = {} override def addChangeListener(changeListener:ChangeListener) :Unit = { // no-op, we don't support state changes } override def removeChangeListener(changeListener:ChangeListener) :Unit = { // no-op, we don't support state changes } private def lexToAst(source:Snapshot, offset:Int) :Int = source match { case null => offset case _ => source.getEmbeddedOffset(offset) } private def astToLex(source:Snapshot, offset:Int) :Int = source match { case null => offset case _ => source.getOriginalOffset(offset) } private def sanitizeSource(context:Context, sanitizing:Sanitize) :Boolean = { false } private def sanitize(context:Context, sanitizing:Sanitize) :ErlangParserResult = { sanitizing match { case NEVER => createParseResult(context) case NONE => createParseResult(context) case _ => // we are out of trick, just return as it createParseResult(context) } } protected def notifyError(context:Context, message:String, sourceName:String, start:Int, lineSource:String, end:Int, sanitizing:Sanitize, severity:Severity, key:String, params:Object) :Unit = { val error = new DefaultError(key, message, null, context.fo, start, end, severity) params match { case null => case x:Array[Object] => error.setParameters(x) case _ => error.setParameters(Array(params)) } context.notifyError(error) if (sanitizing == NONE) { context.errorOffset = start } } protected def parseBuffer(context:Context, sanitizing:Sanitize) :ErlangParserResult = { var sanitizedSource = false var source = context.source sanitizing match { case NONE | NEVER => case _ => val ok = sanitizeSource(context, sanitizing) if (ok) { assert(context.sanitizedSource != null) sanitizedSource = true source = context.sanitizedSource } else { // Try next trick return sanitize(context, sanitizing) } } if (sanitizing == NONE) { context.errorOffset = -1 } val parser = createParser(context) val ignoreErrors = sanitizedSource var root :GNode = null try { var error :ParseError = null val r = parser.pS(0) if (r.hasValue) { val v = r.asInstanceOf[SemanticValue] root = v.value.asInstanceOf[GNode] } else { error = r.parseError } if (error != null && !ignoreErrors) { var start = 0 if (error.index != -1) { start = error.index } notifyError(context, error.msg, "Syntax error", start, "", start, sanitizing, Severity.ERROR, "SYNTAX_ERROR", Array(error)) System.err.println(error.msg) } } catch { case e:IOException => e.printStackTrace case e:IllegalArgumentException => // An internal exception thrown by parser, just catch it and notify notifyError(context, e.getMessage, "", 0, "", 0, sanitizing, Severity.ERROR, "SYNTAX_ERROR", Array(e)) } if (root != null) { context.sanitized = sanitizing context.root = root val r = createParseResult(context) r.setSanitized(context.sanitized, context.sanitizedRange, context.sanitizedContents) r.source = source r } else { sanitize(context, sanitizing) } } protected def createParser(context:Context) :ParserErlang = { val in = new StringReader(context.source) val fileName = if (context.fo != null) context.fo.getNameExt else "" val parser = new ParserErlang(in, fileName) context.parser = parser parser } private def createParseResult(context:Context) :ErlangParserResult = { new ErlangParserResult(this, context.snapshot, context.root, context.th) } /** Parsing context */ class Context(val snapshot:Snapshot, event:SourceModificationEvent) { val errors :List[Error] = new ArrayList[Error] var source :String = ErlangParser.asString(snapshot.getText) var caretOffset :Int = GsfUtilities.getLastKnownCaretOffset(snapshot, event) var root :GNode = _ var th :TokenHierarchy[_] = _ var parser :ParserErlang = _ var errorOffset :Int = _ var sanitizedSource :String = _ var sanitizedRange :OffsetRange = OffsetRange.NONE var sanitizedContents :String = _ var sanitized :Sanitize = NONE def notifyError(error:Error) = errors.add(error) def fo = snapshot.getSource.getFileObject override def toString = "ErlangParser.Context(" + fo + ")" // NOI18N } } object ErlangParser { def asString(sequence:CharSequence) :String = sequence match { case s:String => s case _ => sequence.toString } def sourceUri(source:Source) :String = source.getFileObject match { case null => "fileless" //NOI18N case f => f.getNameExt } } /** Attempts to sanitize the input buffer */ sealed case class Sanitize /** Only parse the current file accurately, don't try heuristics */ case object NEVER extends Sanitize /** Perform no sanitization */ case object NONE extends Sanitize
The major tasks of these two classes are:
- Parsing the source buffer which is passed automatically by the framework when source is modified or just opened, the source text can be got from snapshot.getText. The parsing result used to be an AST tree, you can do semantic/structure analysis on AST tree to get more detailed information later, but as the first step, I just pass/keep AST tree in ErlangParserResult for later usage.
- Notifying the errors to framework. This can be done by store the parsing errors to ParserResult, and implemented getDiagnostics to return the error.
The last step is register the ErlangParser in ErlangLanguage.scala in one line code:
override def getParser = new ErlangParser
Now, open a .erl file, if there is syntax error, the editor will indicate and highlight it now.
My next step is to improve the Rats! definition, add error recover etc.
Erlang Plugin for NetBeans in Scala#2: Set Environment for Writing NetBeans Module in Scala
I'm going to create new Erlang Editor module in Scala, the module project is under http://hg.netbeans.org/main/contrib/file/0caa5d009839/erlang.editor/ which I just committed in.
The first step is to get Scala source file mixed in Java based NetBeans source/project tree. After define project's manifest,mf, we'll create a special build.xml for this project under erlang.editor directory:
build.xml
<?xml version="1.0" encoding="UTF-8"?> <project name="contrib/erlang.editor" default="netbeans" basedir="."> <import file="../../nbbuild/templates/projectized.xml"/> <import file="scala-build.xml"/> <!-- special jar target for csl --> <target name="jar" depends="init,compile,jar-prep" unless="is.jar.uptodate"> <taskdef name="csljar" classname="org.netbeans.modules.csl.CslJar" classpath="${nb_all}/csl.api/anttask/build/cslanttask.jar:${nb_all}/nbbuild/nbantext.jar"/> <csljar jarfile="${cluster}/${module.jar}" compress="${build.package.compress}" index="${build.package.index}" manifest="${manifest.mf}" stamp="${cluster}/.lastModified"> <fileset dir="${build.classes.dir}"/> </csljar> </target> <target name="compile" depends="init,projectized-common.compile,scala-compile"/> <target name="do-test-build" depends="init,test-init,projectized-common.do-test-build"/> <target name="rats" depends="init" description="Scanner"> <echo message="Rebuilding token scanner... ${rats.package.dir}"/> <java fork="yes" dir="${src.dir}/${rats.package.dir}" classname="xtc.parser.Rats" classpath="${rats.jar}"> <arg value="-in"/> <arg value="${src.dir}"/> <arg value="${rats.lexer.file}"/> </java> </target> </project>
This above build.xml imported NetBeans' main template ant xml "../../nbbuild/templates/projectized.xml" and a special scala-build.xml file, which defines scalac and javac task for mixed Scala/Java module for NetBeans. You can ignore the "rats" target if you has no plan to write language's lexer/parser in Rats! parser generator. scala-build.xml looks like:
scala-build.xml
<?xml version="1.0" encoding="UTF-8"?> <project name="scala-module" default="netbeans" basedir="."> <import file="../../nbbuild/templates/projectized.xml"/> <target name="scala-taskdef" depends="init"> <echo message="Compiling scala sources via ${scala.library}, ${scala.compiler}"/> <taskdef resource="scala/tools/ant/antlib.xml"> <classpath> <pathelement location="${scala.library}"/> <pathelement location="${scala.compiler}"/> </classpath> </taskdef> </target> <property name="jar-excludes" value="**/*.java,**/*.form,**/package.html,**/doc-files/,**/*.scala"/> <target name="scala-compile" depends="init,up-to-date,scala-taskdef" unless="is.jar.uptodate"> <!-- javac's classpath should include scala.library and all these paths of "cp" --> <path id="javac.cp"> <pathelement path="${scala.libs}"/> <pathelement path="${module.classpath}"/> <pathelement path="${cp.extra}"/> </path> <!-- scalac will check class dependencies deeply, so we can not rely on public package only which is refed by ${module.classpath} --> <path id="scalac.cp"> <pathelement path="${scala.libs}"/> <pathelement path="${module.run.classpath}"/> <pathelement path="${cp.extra}"/> </path> <mkdir dir="${build.classes.dir}"/> <depend srcdir="${src.dir}" destdir="${build.classes.dir}" cache="build/depcache"> <classpath refid="scalac.cp"/> </depend> <!-- scalac --> <scalac srcdir="${src.dir}" destdir="${build.classes.dir}" encoding="UTF-8" target="jvm-${javac.target}"> <classpath refid="scalac.cp"/> </scalac> <!-- javac --> <nb-javac srcdir="${src.dir}" destdir="${build.classes.dir}" debug="${build.compiler.debug}" debuglevel="${build.compiler.debuglevel}" encoding="UTF-8" deprecation="${build.compiler.deprecation}" optimize="${build.compiler.optimize}" source="${javac.source}" target="${javac.target}" includeantruntime="false"> <classpath refid="javac.cp"/> <compilerarg line="${javac.compilerargs}"/> <processorpath refid="processor.cp"/> </nb-javac> <!-- Sanity check: --> <pathconvert pathsep=":" property="class.files.in.src"> <path> <fileset dir="${src.dir}"> <include name="**/*.class"/> </fileset> </path> </pathconvert> <fail> <condition> <not> <equals arg1="${class.files.in.src}" arg2=""/> </not> </condition> You have stray *.class files in ${src.dir} which you must remove. Probably you failed to clean your sources before updating them. </fail> <!-- OK, continue: --> <copy todir="${build.classes.dir}"> <fileset dir="${src.dir}" excludes="${jar-excludes}"/> </copy> </target> <target name="do-test-build" depends="projectized-common.do-test-build"> <scalac srcdir="${test.unit.src.dir}" destdir="${build.test.unit.classes.dir}" excludes="${test.excludes}" encoding="UTF-8"> <classpath refid="test.unit.cp"/> </scalac> </target> </project>
You need also to set the project's module dependencies on:
- org.netbeans.libs.scala
- org.netbeans.modules.csl.api
- org.netbeans.modules.lexer
- org.netbeans.modules.parsing.api
- org.openide.filesystems
And, the nbproject/project.properties which defines some important project building properties, such as:
javac.compilerargs=-Xlint:unchecked javac.source=1.5 nbm.homepage=http://wiki.netbeans.org/Erlang scala.library=${cluster}/modules/ext/scala-library-2.7.3.jar scala.compiler=${cluster}/modules/ext/scala-compiler-2.7.3.jar scala.libs=\ ${scala.library}:\ ${scala.compiler}
You can write NetBeans' module in Scala now.
Erlang Plugin for NetBeans in Scala#3: Minimal Lexer Intergation
>>> Updated Feb 8:
Code fixed to avoid an infinite cycled scanning when source file size > 16k
===
The minim supporting is to integrate a language lexer to NetBeans's language support framework. As NetBeans 7.0, there is a new effort for common language supporting, which is called CSL (Common Scripting Language). Don't be confused by this module's name, it not only for scripting language, actually, Java support has been migrated to CSL in 7.0. There are discussions on a better name. CSL is forked and created on GSF (Generic Scripting Framework) as a GSF's new variant that is based on new Parsing & Indexing API.
Since I'm going to write an Erlang lexer in Rats! generator parser, I need to add dependency on rats run-time libs first, rats run-time module is under contrib/xtc, which I patched to support end position of each production.
Then, you have to tell the project where to find the rats! libs, this can be done by adding following properties to nbproject/project.properties, now this nbproject/project.properties looks like:
nbproject/project.properties
javac.compilerargs=-Xlint:unchecked javac.source=1.5 nbm.homepage=http://wiki.netbeans.org/Erlang scala.library=${cluster}/modules/ext/scala-library-2.7.3.jar scala.compiler=${cluster}/modules/ext/scala-compiler-2.7.3.jar scala.libs=\ ${scala.library}:\ ${scala.compiler} rats.jar=${cluster}/modules/xtc.jar rats.package.dir=org/netbeans/modules/erlang/editor/rats rats.lexer.file=LexerErlang.rats
All Rats! definitions of Erlang token can be found at http://hg.netbeans.org/main/contrib/file/tip/erlang.editor/src/org/netbeans/modules/erlang/editor/rats/. Don't ask me how to write Rats! rules for languages, you should get these information from Rats! web site. Or, you can integrate other types of lexer generated by other lexer generator, the examples can be found in NetBeans' other languages supporting modules.
The Erlang's lexer will be generated via "rats.lexer.file=LexerErlang.rats", which is the entry point of all defined rules for Erlang tokens. Run "rats" target will generate a LexerErlang.java file which is the lexer class that will be used to create Erlang tokens from Erlang source files.
Now, we should integrate this lexer class to NetBeans' lexer engine, this is done by two Scala files:
ErlangLexer.scala
/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 1997-2008 Sun Microsystems, Inc. All rights reserved. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common * Development and Distribution License("CDDL") (collectively, the * "License"). You may not use this file except in compliance with the * License. You can obtain a copy of the License at * http://www.netbeans.org/cddl-gplv2.html * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the * specific language governing permissions and limitations under the * License. When distributing the software, include this License Header * Notice in each file and include the License file at * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this * particular file as subject to the "Classpath" exception as provided * by Sun in the GPL Version 2 section of the License file that * accompanied this code. If applicable, add the following below the * License Header, with the fields enclosed by brackets [] replaced by * your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" * * Contributor(s): * * The Original Software is NetBeans. The Initial Developer of the Original * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun * Microsystems, Inc. All Rights Reserved. * * If you wish your version of this file to be governed by only the CDDL * or only the GPL Version 2, indicate your decision by adding * "[Contributor] elects to include this software in this distribution * under the [CDDL or GPL Version 2] license." If you do not indicate a * single choice of license, a recipient has the option to distribute * your version of this file under either the CDDL, the GPL Version 2 or * to extend the choice of license to its licensees as provided above. * However, if you add GPL Version 2 code and therefore, elected the GPL * Version 2 license, then the option applies only if the new code is * made subject to such option by the copyright holder. */ package org.netbeans.modules.erlang.editor.lexer import _root_.java.io.IOException import _root_.java.io.Reader import org.netbeans.api.lexer.{Token, TokenId} import org.netbeans.modules.erlang.editor.rats.LexerErlang import org.netbeans.spi.lexer.Lexer import org.netbeans.spi.lexer.LexerInput import org.netbeans.spi.lexer.LexerRestartInfo import org.netbeans.spi.lexer.TokenFactory import xtc.parser.Result import xtc.tree.GNode import xtc.util.Pair import scala.collection.mutable.ArrayBuffer import org.netbeans.modules.erlang.editor.lexer.ErlangTokenId._ /** * * @author Caoyuan Deng */ object ErlangLexer { /** @Note: * ErlangLexer class is not Reentrant safe, it seems when source size is large than 16 * 1024, * there will be more than one input are used, which causes the offset states, such as readed * token length, offset etc in these inputs conflict?. Anyway it's safe to create a new one always. */ def create(info:LexerRestartInfo[TokenId]) = new ErlangLexer(info) } class ErlangLexer(info:LexerRestartInfo[TokenId]) extends Lexer[TokenId] { /** @Note: * it seems input at this time is empty, so we can not do scanning here. * input will be filled in chars when call nextToken */ var input :LexerInput = info.input var tokenFactory :TokenFactory[TokenId] = info.tokenFactory var lexerInputReader :LexerInputReader = new LexerInputReader(input) val tokenStream = new ArrayBuffer[TokenInfo] // * tokenStream.elements always return a new iterator, which point the first // * item, so we should have a global one. var tokenStreamItr :Iterator[TokenInfo] = tokenStream.elements var lookahead :Int = 0 override def release = {} override def state :Object = null override def nextToken :Token[TokenId] = { // * In case of embedded tokens, there may be tokens that had been scanned // * but not taken yet, check first if (!tokenStreamItr.hasNext) { tokenStream.clear scanTokens tokenStreamItr = tokenStream.elements /** * @Bug of LexerInput.backup(int) ? * backup(0) will cause input.readLength() increase 1 */ lookahead = input.readLength if (lookahead > 0) { // * backup all, we will re-read from begin to create token at following step input.backup(lookahead) } else { return null } } if (tokenStreamItr.hasNext) { val tokenInfo = tokenStreamItr.next if (tokenInfo.length == 0) { // * EOF return null } // * read token's chars according to tokenInfo.length var i = 0 while (i < tokenInfo.length) { input.read i += 1 } // * see if needs to lookahead, if true, perform it lookahead -= tokenInfo.length // * to cheat incremently lexer, we needs to lookahead one more char when // * tokenStream.size() > 1 (batched tokens that are not context free), // * so, when modification happens extractly behind latest token, will // * force lexer relexer from the 1st token of tokenStream val lookahead1 = if (tokenStream.size > 1) lookahead + 1 else lookahead if (lookahead1 > 0) { var i = 0 while (i < lookahead1) { input.read i += 1 } input.backup(lookahead1) } val tokenLength = input.readLength createToken(tokenInfo.id, tokenLength) } else { assert(false, "unrecognized input" + input.read) null } } def createToken(id:TokenId, length:Int) :Token[TokenId] = id.asInstanceOf[ErlangTokenId].fixedText match { case null => tokenFactory.createToken(id, length) case fixedText => tokenFactory.getFlyweightToken(id, fixedText) } def scanTokens :Result = { /** * We cannot keep an instance scope lexer, since lexer (sub-class of ParserBase) * has internal states which keep the read-in chars, index and others, it really * difficult to handle. */ val scanner = new LexerErlang(lexerInputReader, "") try { // * just scan from position 0, incrmental lexer engine will handle start char in lexerInputReader val r = scanner.pToken(0) if (r.hasValue) { val node = r.semanticValue.asInstanceOf[GNode] flattenToTokenStream(node) r } else { System.err.println(r.parseError.msg) null } } catch { case e:Exception => System.err.println(e.getMessage) null } } def flattenToTokenStream(node:GNode) :Unit = { val l = node.size if (l == 0) { /** @Note: * When node.size == 0, it's a void node. This should be limited to * EOF when you define lexical rats. * * And in Rats!, EOF is !_, the input.readLength() will return 0 */ assert(input.readLength == 0, "This generic node: " + node.getName + " is a void node, this should happen only on EOF. Check you rats file.") val tokenInfo = new TokenInfo(0, null) tokenStream += tokenInfo return } var i = 0 while (i < l) { node.get(i) match { case null => // * child may be null case child:GNode => flattenToTokenStream(child) case child:Pair[_] => assert(false, "Pair:" + child + " to be process, do you add 'flatten' option on grammar file?") case child:String => val length = child.length val id = ErlangTokenId.valueOf(node.getName) match { case None => ErlangTokenId.IGNORED case Some(v) => v.asInstanceOf[TokenId] } val tokenInfo = new TokenInfo(length, id) tokenStream += tokenInfo case child => println("To be process: " + child) } i += 1 } } /** * Hacking for xtc.parser.ParserBase
of Rats! which usejava.io.Reader
* as the chars input, but uses only {@link java.io.Reader#read()} of all methods in * {@link xtc.parser.ParserBase#character(int)} */ class LexerInputReader(input:LexerInput) extends Reader { override def read :Int = input.read match { case LexerInput.EOF => -1 case c => c } override def read(cbuf:Array[Char], off:Int, len:Int) :Int = { throw new UnsupportedOperationException("Not supported yet.") } override def close = {} } class TokenInfo(val length:Int, val id:TokenId) { override def toString = "(id=" + id + ", length=" + length + ")" } }
ErlangTokenId
/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common * Development and Distribution License("CDDL") (collectively, the * "License"). You may not use this file except in compliance with the * License. You can obtain a copy of the License at * http://www.netbeans.org/cddl-gplv2.html * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the * specific language governing permissions and limitations under the * License. When distributing the software, include this License Header * Notice in each file and include the License file at * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this * particular file as subject to the "Classpath" exception as provided * by Sun in the GPL Version 2 section of the License file that * accompanied this code. If applicable, add the following below the * License Header, with the fields enclosed by brackets [] replaced by * your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" * * Contributor(s): * * The Original Software is NetBeans. The Initial Developer of the Original * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun * Microsystems, Inc. All Rights Reserved. * * If you wish your version of this file to be governed by only the CDDL * or only the GPL Version 2, indicate your decision by adding * "[Contributor] elects to include this software in this distribution * under the [CDDL or GPL Version 2] license." If you do not indicate a * single choice of license, a recipient has the option to distribute * your version of this file under either the CDDL, the GPL Version 2 or * to extend the choice of license to its licensees as provided above. * However, if you add GPL Version 2 code and therefore, elected the GPL * Version 2 license, then the option applies only if the new code is * made subject to such option by the copyright holder. */ package org.netbeans.modules.erlang.editor.lexer import _root_.java.util.Collection import _root_.java.util.Collections import _root_.java.util.HashMap import _root_.java.util.HashSet import _root_.java.util.Map import _root_.java.util.Arrays import org.netbeans.api.lexer.InputAttributes import org.netbeans.api.lexer.Language import org.netbeans.api.lexer.LanguagePath import org.netbeans.api.lexer.Token import org.netbeans.api.lexer.TokenId import org.netbeans.spi.lexer.LanguageEmbedding import org.netbeans.spi.lexer.LanguageHierarchy import org.netbeans.spi.lexer.Lexer import org.netbeans.spi.lexer.LexerRestartInfo /** * * @author Caoyuan Deng */ object ErlangTokenId extends Enumeration { // Let type of enum's value the same as enum itself type ErlangTokenId = V // Extends Enumeration.Val to get custom enumeration value class V(val name:String, val fixedText:String, val primaryCategory:String) extends Val(name) with TokenId { override def ordinal = id } object V { def apply(name:String, fixedText:String, primaryCategory:String) = new V(name, fixedText, primaryCategory) } val IGNORED = V("IGNORED", null, "ingore") val Error = V("Error", null, "error") // --- Spaces and comments val Ws = V("Ws", null, "whitespace") val Nl = V("Nl", null, "whitespace") val LineComment = V("LineComment", null, "comment") val CommentTag = V("CommentTag", null, "comment") val CommentData = V("CommentData", null, "comment") // --- Literals val IntegerLiteral = V("IntegerLiteral", null, "number") val FloatingPointLiteral = V("FloatingPointLiteral", null, "number") val CharacterLiteral = V("CharacterLiteral", null, "char") val StringLiteral = V("StringLiteral", null, "string") // --- Keywords val Andalso = V("Andalso", "andalso", "keyword") val After = V("After", "after", "keyword") val And = V("And", "and", "keyword") val Band = V("Band", "band", "keyword") val Begin = V("Begin", "begin", "keyword") val Bnot = V("Bnot", "bnot", "keyword") val Bor = V("Bor", "bor", "keyword") val Bsr = V("Bsr", "bsr", "keyword") val Bxor = V("Bxor", "bxor", "keyword") val Case = V("Case", "case", "keyword") val Catch = V("Catch", "catch", "keyword") val Cond = V("Cond", "cond", "keyword") val Div = V("Div", "div", "keyword") val End = V("End", "end", "keyword") val Fun = V("Fun", "fun", "keyword") val If = V("If", "if", "keyword") val Not = V("Not", "not", "keyword") val Of = V("Of", "of", "keyword") val Orelse = V("Orelse", "orelse", "keyword") val Or = V("Or", "or", "keyword") val Query = V("Query", "query", "keyword") val Receive = V("Receive", "receive", "keyword") val Rem = V("Rem", "rem", "keyword") val Try = V("Try", "try", "keyword") val Spec = V("Spec", "spec", "keyword") val When = V("When", "when", "keyword") val Xor = V("Xor", "xor", "keyword") // --- Identifiers val Macro = V("Macro", null, "identifier") val Atom = V("Atom", null, "identifier") val Var = V("Var", null, "identifier") val Rec = V("Rec", null, "identifier") // --- Stop val Stop = V("Stop", ".", "separator") // --- Symbols val LParen = V("LParen", "(", "separator") val RParen = V("RParan", ")", "separator") val LBrace = V("LBrace", "{", "separator") val RBrace = V("RBrace", "}", "separator") val LBracket = V("LBracket", "[", "separator") val RBracket = V("RBracket", "]", "separator") val Comma = V("Comma", ",", "separator") val Dot = V("Dot", ".", "separator") val Semicolon = V("Semicolon", ";", "separator") val DBar = V("DBar", "||", "separator") val Bar = V("Bar", "|", "separator") val Question = V("Question", "?","separator") val DLt = V("DLt", "<<", "separator") val LArrow = V("LArrow", "<-", "separator") val Lt = V("Lt", "<", "separator") val DGt = V("DGt", ">>", "separator") val Ge = V("Ge", ">=", "separator") val Gt = V("Gt", ">", "separator") val ColonMinus = V("ColonMinus", ":-", "separator") val DColon = V("DColon", "::", "separator") val Colon = V("Colon", ":", "separator") val Hash = V("Hash", "#", "separator") val DPlus = V("DPlus", "++", "separator") val Plus = V("Plus", "+", "separator") val DMinus = V("DMinus", "--", "separator") val RArrow = V("RArrow", "->", "separator") val Minus = V("Minus", "-", "separator") val Star = V("Star", "*", "separator") val Ne = V("Ne", "/=", "separator") val Slash = V("Slash", "/", "separator") val EEq = V("EEq", "=:=", "separator") val ENe = V("ENe", "=/=", "separator") val DEq = V("DEq", "==", "separator") val Le = V("le", "=<", "separator") val Eq = V("Eq", "=", "separator") val Exclamation = V("Exclamation", "!", "separator") /** * MIME type for Erlang. Don't change this without also consulting the various XML files * that cannot reference this value directly. */ val ERLANG_MIME_TYPE = "text/x-erlang"; // NOI18N // * should use "val" instead of "def" here to get a singleton language val, which // * will be used to identity the token's language by "==" comparasion by other classes. // * Be aware of the init order! to get createTokenIds gathers all TokenIds, should // * be put after all token id val definition val language = new LanguageHierarchy[TokenId] { protected def mimeType = ERLANG_MIME_TYPE protected def createTokenIds :Collection[TokenId] = { val ids = new HashSet[TokenId] elements.foreach{ids add _.asInstanceOf[TokenId]} ids } protected def createLexer(info:LexerRestartInfo[TokenId]) :Lexer[TokenId] = ErlangLexer.create(info) override protected def createTokenCategories :Map[String, Collection[TokenId]] = { val cats = new HashMap[String, Collection[TokenId]] cats } override protected def embedding(token:Token[TokenId], languagePath:LanguagePath, inputAttributes:InputAttributes) = { null // No embedding } }.language }
ErlangTokenId implemented org.netbeans.api.lexer.TokenId, and defined all Erlang token's Ids, with each name, id, fixedText and primaryCategory.
ErlangLexer.scala is the bridge between LexerErlang.java and NetBeans' lexer engine. NetBeans' lexer engine is an incremental engine, which will automatically handle the positions when you insert/modify a char, wrap the sanitary buffer in a LexerInput and pass it to lexer. I have carefully degined ErlangLexer.scala to get these benefits, you can use this file for other languages too, if you have understood how I write rats rules for tokens.
Now, you should implemented an org.netbeans.modules.csl.spi.DefaultLanguageConfig, which register all services for your language, such as: CodeCompletionHandler, DeclarationFinder, Formatter, IndexSearcher, InstantRenamer, KeystrokeHandler, OccurrencesFinder, SemanticAnalyzer, StructureScanner etc. For the first step, we only implemented a minim supporting for Erlang, which actually is only a lexer to get Erlang tokens and highlight them. So, our implementation is fairly simple:
ErlangLanguage.scala
/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common * Development and Distribution License("CDDL") (collectively, the * "License"). You may not use this file except in compliance with the * License. You can obtain a copy of the License at * http://www.netbeans.org/cddl-gplv2.html * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the * specific language governing permissions and limitations under the * License. When distributing the software, include this License Header * Notice in each file and include the License file at * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this * particular file as subject to the "Classpath" exception as provided * by Sun in the GPL Version 2 section of the License file that * accompanied this code. If applicable, add the following below the * License Header, with the fields enclosed by brackets [] replaced by * your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" * * Contributor(s): * * The Original Software is NetBeans. The Initial Developer of the Original * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun * Microsystems, Inc. All Rights Reserved. * * If you wish your version of this file to be governed by only the CDDL * or only the GPL Version 2, indicate your decision by adding * "[Contributor] elects to include this software in this distribution * under the [CDDL or GPL Version 2] license." If you do not indicate a * single choice of license, a recipient has the option to distribute * your version of this file under either the CDDL, the GPL Version 2 or * to extend the choice of license to its licensees as provided above. * However, if you add GPL Version 2 code and therefore, elected the GPL * Version 2 license, then the option applies only if the new code is * made subject to such option by the copyright holder. */ package org.netbeans.modules.erlang.editor import _root_.java.io.File import _root_.java.util.Collection import _root_.java.util.Collections import _root_.java.util.HashMap import _root_.java.util.Map import _root_.java.util.Set import org.netbeans.api.lexer.Language; import org.netbeans.modules.csl.api.CodeCompletionHandler import org.netbeans.modules.csl.api.DeclarationFinder import org.netbeans.modules.csl.api.Formatter import org.netbeans.modules.csl.api.IndexSearcher import org.netbeans.modules.csl.api.InstantRenamer import org.netbeans.modules.csl.api.KeystrokeHandler import org.netbeans.modules.csl.api.OccurrencesFinder import org.netbeans.modules.csl.api.SemanticAnalyzer import org.netbeans.modules.csl.api.StructureScanner import org.netbeans.modules.csl.spi.DefaultLanguageConfig import org.netbeans.modules.parsing.spi.Parser import org.netbeans.modules.parsing.spi.indexing.EmbeddingIndexerFactory import org.openide.filesystems.FileObject import org.openide.filesystems.FileUtil import org.netbeans.modules.erlang.editor.lexer.ErlangTokenId /* * Language/lexing configuration for Erlang * * @author Caoyuan Deng */ class ErlangLanguage extends DefaultLanguageConfig { override def getLexerLanguage = ErlangTokenId.language override def getDisplayName : String = "Erlang" override def getPreferredExtension : String = { "erl" // NOI18N } }where def getLexerLanguage = ErlangTokenId.language is exact the LanguageHierarchy implementation for ErlangTokenId in ErlangTokenId.scala, which will tell the framework about token ids, category, embedding information.
The final step is to register ErlangLanguage and fontColor.xml, erlangResolver.xml etc in layer.xml for color highlights, mime resolver and language icon. All these necessary resource files are under: http://hg.netbeans.org/main/contrib/file/0caa5d009839/erlang.editor/src/org/netbeans/modules/erlang/editor/resources/
layer.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE filesystem PUBLIC "-//NetBeans//DTD Filesystem 1.1//EN" "http://www.netbeans.org/dtds/filesystem-1_1.dtd"> <filesystem> <folder name="Services"> <folder name="MIMEResolver"> <file name="Erlang.xml" url="erlangResolver.xml"> <attr name="SystemFileSystem.localizingBundle" stringvalue="org.netbeans.modules.erlang.editor.resources.Bundle"/> <attr name="position" intvalue="1005"/> </file> </folder> </folder> <folder name="Editors"> <!-- Reference binding color themes are under module: main/defaults/src/org/netbeans/modules/defaults --> <!-- color theme for nbeditor-settings-ColoringType --> <folder name="FontsColors"> <folder name="Twilight"> <folder name="Defaults"> <file name="org-netbeans-modules-defaults-highlight-colorings.xml" url="Twilight/editor.xml"> <attr name="SystemFileSystem.localizingBundle" stringvalue="org.netbeans.modules.defaults.Bundle"/> <attr name="nbeditor-settings-ColoringType" stringvalue="highlight"/> </file> <file name="org-netbeans-modules-defaults-token-colorings.xml" url="Twilight/defaults.xml"> <attr name="SystemFileSystem.localizingBundle" stringvalue="org.netbeans.modules.defaults.Bundle"/> </file> </folder> </folder> <folder name="EmacsStandard"> <folder name="Defaults"> <file name="org-netbeans-modules-defaults-token-colorings.xml" url="EmacsStandard/defaults.xml"> <attr name="SystemFileSystem.localizingBundle" stringvalue="org.netbeans.modules.defaults.Bundle"/> </file> </folder> </folder> </folder> <folder name="text"> <folder name="x-erlang"> <attr name="SystemFileSystem.localizingBundle" stringvalue="org.netbeans.modules.erlang.editor.Bundle"/> <file name="language.instance"> <attr name="instanceCreate" methodvalue="org.netbeans.modules.erlang.editor.lexer.ErlangTokenId.language"/> <attr name="instanceOf" stringvalue="org.netbeans.api.lexer.Language"/> </file> <!-- TODO - this should not be necessary; I'm doing this now to work around bugs in color initialization --> <folder name="FontsColors"> <folder name="NetBeans"> <folder name="Defaults"> <file name="coloring.xml" url="fontsColors.xml"> <attr name="SystemFileSystem.localizingBundle" stringvalue="org.netbeans.modules.erlang.editor.Bundle"/> </file> </folder> </folder> <folder name="Twilight"> <folder name="Defaults"> <file name="coloring.xml" url="Twilight/fontsColors.xml"> <attr name="SystemFileSystem.localizingBundle" stringvalue="org.netbeans.modules.erlang.editor.Bundle"/> </file> </folder> </folder> <folder name="EmacsStandard"> <folder name="Defaults"> <file name="coloring.xml" url="EmacsStandard/fontsColors.xml"> <attr name="SystemFileSystem.localizingBundle" stringvalue="org.netbeans.modules.erlang.editor.Bundle"/> </file> </folder> </folder> </folder> <folder name="CodeTemplates"> <folder name="Defaults"> <file name="codeTemplates.xml" url="codeTemplates.xml"> <attr name="SystemFileSystem.localizingBundle" stringvalue="org.netbeans.modules.erlang.editor.Bundle"/> </file> </folder> </folder> <folder name="Keybindings"> <folder name="NetBeans"> <folder name="Defaults"> <file name="org-netbeans-modules-erlang-editor-keybindings.xml" url="keyBindings.xml"/> </folder> </folder> </folder> </folder> </folder> </folder> <folder name="CslPlugins"> <folder name="text"> <folder name="x-erlang"> <file name="language.instance"> <attr name="instanceClass" stringvalue="org.netbeans.modules.erlang.editor.ErlangLanguage"/> </file> <!--file name="structure.instance"> <attr name="instanceClass" stringvalue="org.netbeans.modules.scala.editing.ScalaStructureAnalyzer"/> </file--> </folder> </folder> </folder> <folder name="Loaders"> <folder name="text"> <folder name="x-erlang"> <attr name="SystemFileSystem.icon" urlvalue="nbresloc:/org/netbeans/modules/erlang/editor/resources/Erlang.png"/> <attr name="iconBase" stringvalue="org/netbeans/modules/erlang/editor/resources/Erlang.png"/> <folder name="Actions"> <file name="OpenAction.instance"> <attr name="instanceClass" stringvalue="org.openide.actions.OpenAction"/> <attr name="position" intvalue="100"/> </file> <file name="Separator1.instance"> <attr name="instanceClass" stringvalue="javax.swing.JSeparator"/> <attr name="position" intvalue="200"/> </file> <file name="CutAction.instance"> <attr name="instanceClass" stringvalue="org.openide.actions.CutAction"/> <attr name="position" intvalue="300"/> </file> <file name="CopyAction.instance"> <attr name="instanceClass" stringvalue="org.openide.actions.CopyAction"/> <attr name="position" intvalue="400"/> </file> <file name="PasteAction.instance"> <attr name="instanceClass" stringvalue="org.openide.actions.PasteAction"/> <attr name="position" intvalue="500"/> </file> <file name="Separator2.instance"> <attr name="instanceClass" stringvalue="javax.swing.JSeparator"/> <attr name="position" intvalue="600"/> </file> <file name="NewAction.instance"> <attr name="instanceClass" stringvalue="org.openide.actions.NewAction"/> <attr name="position" intvalue="700"/> </file> <file name="DeleteAction.instance"> <attr name="instanceClass" stringvalue="org.openide.actions.DeleteAction"/> <attr name="position" intvalue="800"/> </file> <file name="RenameAction.instance"> <attr name="instanceClass" stringvalue="org.openide.actions.RenameAction"/> <attr name="position" intvalue="900"/> </file> <file name="Separator3.instance"> <attr name="instanceClass" stringvalue="javax.swing.JSeparator"/> <attr name="position" intvalue="1000"/> </file> <file name="SaveAsTemplateAction.instance"> <attr name="instanceClass" stringvalue="org.openide.actions.SaveAsTemplateAction"/> <attr name="position" intvalue="1100"/> </file> <file name="Separator4.instance"> <attr name="instanceClass" stringvalue="javax.swing.JSeparator"/> <attr name="position" intvalue="1200"/> </file> <file name="FileSystemAction.instance"> <attr name="instanceClass" stringvalue="org.openide.actions.FileSystemAction"/> <attr name="position" intvalue="1300"/> </file> <file name="Separator5.instance"> <attr name="instanceClass" stringvalue="javax.swing.JSeparator"/> <attr name="position" intvalue="1400"/> </file> <file name="ToolsAction.instance"> <attr name="instanceClass" stringvalue="org.openide.actions.ToolsAction"/> <attr name="position" intvalue="1500"/> </file> <file name="PropertiesAction.instance"> <attr name="instanceClass" stringvalue="org.openide.actions.PropertiesAction"/> <attr name="position" intvalue="1600"/> </file> </folder> </folder> </folder> </folder> </filesystem>
Now build your new module, lunch it, and open a .erl file, you get:
Click on the picture to enlarge it
Erlang Plugin for NetBeans in Scala#1: Scala Enumeration Implemented a Java Interface
ErlyBird (previous plugin) was written in Java, and based on NetBeans' Generic Languages Framework ( Project Schliemann). As NetBeans 7.0 introducing new Parsing API, I'm going to migrate Erlang plugin for NetBeans to it, and rewrite in Scala.
Defining a Scala/Java mixed module project in NetBeans' trunk tree is a bit trick, but, since it's plain ant-based project, you can always take over it by writing/modifying build.xml. Challenges are actually from integrating Scala's Class/Object/Trait to an already existed framework. I met this kind of challenge quickly when I tried to implement an enum like class which should implement a Java interface org.netbeans.api.lexer.TokenId?
The signature of TokenId? is:
public interface TokenId { String name(); int ordinal(); String primaryCategory(); }
From the signature, you can get the hint to implement it as a Java enum. That's true and straightforward in Java. But how about in Scala?
There is an abstract class Enumeration in Scala's library. The example code shows how to use it by extending it as a singleton Object. But for my case, I have to implement name() and ordinal() methods of TokenId? in the meantime, which brings some inconveniences for get the name() method automatically/magically for each enumeration value.
A quick google search gave some discussions about it, for example, a method reflection and invocation may be a choice to get name() simply implemented. I tried it, but finally dropped, because you cannot guarantee the condition of call stack of name(), it may happen to be called in this Object's own method, which then, may bring infinite cycled calls.
My final code is like:
package org.netbeans.modules.erlang.editor.lexer object ErlangTokenId extends Enumeration { type ErlangTokenId = V // Extends Enumeration's inner class Val to get custom enumeration value class V(val name:String, val fixedText:String, val primaryCategory:String) extends Val(name) with TokenId { override def ordinal = id } object V { def apply(name:String, fixedText:String, primaryCategory:String) = new V(name, fixedText, primaryCategory) } val IGNORED = V("IGNORED", null, "ingore") val Error = V("Error", null, "error") // --- Spaces and comments val Ws = V("Ws", null, "whitespace") val Nl = V("Nl", null, "whitespace") val LineComment = V("LineComment", null, "comment") val CommentTag = V("CommentTag", null, "comment") val CommentData = V("CommentData", null, "comment") // --- Literals val IntegerLiteral = V("IntegerLiteral", null, "number") val FloatingPointLiteral = V("FloatingPointLiteral", null, "number") val CharacterLiteral = V("CharacterLiteral", null, "char") val StringLiteral = V("StringLiteral", null, "string") // --- Keywords val Andalso = V("Andalso", "andalso", "keyword") val After = V("After", "after", "keyword") val And = V("And", "and", "keyword") val Band = V("Band", "band", "keyword") val Begin = V("Begin", "begin", "keyword") val Bnot = V("Bnot", "bnot", "keyword") val Bor = V("Bor", "bor", "keyword") val Bsr = V("Bsr", "bsr", "keyword") val Bxor = V("Bxor", "bxor", "keyword") val Case = V("Case", "case", "keyword") val Catch = V("Catch", "catch", "keyword") val Cond = V("Cond", "cond", "keyword") val Div = V("Div", "div", "keyword") val End = V("End", "end", "keyword") val Fun = V("Fun", "fun", "keyword") val If = V("If", "if", "keyword") val Not = V("Not", "not", "keyword") val Of = V("Of", "of", "keyword") val Orelse = V("Orelse", "orelse", "keyword") val Or = V("Or", "or", "keyword") val Query = V("Query", "query", "keyword") val Receive = V("Receive", "receive", "keyword") val Rem = V("Rem", "rem", "keyword") val Try = V("Try", "try", "keyword") val When = V("When", "when", "keyword") val Xor = V("Xor", "xor", "keyword") // --- Identifiers val Atom = V("Atom", null, "identifier") val Var = V("Var", null, "identifier") // --- Symbols val LParen = V("LParen", "(", "separator") val RParen = V("RParan", ")", "separator") val LBrace = V("LBrace", "{", "separator") val RBrace = V("RBrace", "}", "separator") val LBracket = V("LBracket", "[", "separator") val RBracket = V("RBracket", "]", "separator") val Comma = V("Comma", ",", "separator") val Dot = V("Dot", ".", "separator") val Semicolon = V("Semicolon", ";", "separator") val DBar = V("DBar", "||", "separator") val Bar = V("Bar", "|", "separator") val Question = V("Question", "?","separator") val DLt = V("DLt", "<<", "separator") val LArrow = V("LArrow", "<-", "separator") val Lt = V("Lt", "<", "separator") val DGt = V("DGt", >", "separator") val Ge = V("Ge", =", "separator") val Gt = V("Gt", ", "separator") val ColonMinus = V("ColonMinus", ":-", "separator") val DColon = V("DColon", "::", "separator") val Colon = V("Colon", ":", "separator") val Hash = V("Hash", "#", "separator") val DPlus = V("DPlus", "++", "separator") val Plus = V("Plus", "+", "separator") val DMinus = V("DMinus", "--", "separator") val RArrow = V("RArrow", "->", "separator") val Minus = V("Minus", "-", "separator") val Star = V("Star", "*", "separator") val Ne = V("Ne", "/=", "separator") val Slash = V("Slash", "/", "separator") val EEq = V("EEq", "=:=", "separator") val ENe = V("ENe", "=/=", "separator") val DEq = V("DEq", "==", "separator") val Le = V("le", "=<", "separator") val Eq = V("Eq", "=", "separator") val Exclamation = V("Exclamation", "!", "separator") }
First, I defined a class V which extends Enumeration.Val, and implemented TokenId with an extra field: fixedText.
Then, I have to explicitly put the value's name to this class and pass it to Enumeration.Val's constructor, so function ErlangTokenId.valueOf(String) will work as Java's enum type.
By type ErlangTokenId = V, type ErlangTokenId.V is now aliased as ErlangTokenId, so you can use ErlangTokenId instead of ErlangTokenId.V everywhere now, which exactly gets the effect of one of the behaviors of Java's enum: enum's value is the same type of enum itself.
Scala Plugin 0.15.1 for NetBeans Released
- Bundling with Scala 2.7.3
- Partly supported Java/Scala mixed project: building project is OK (need to create a new Scala project); Java source is visible in Scala editor.
- Various bugs fixes
To download, please go to: https://sourceforge.net/project/showfiles.php?group_id=192439&package_id=256544&release_id=654650
For more information, please see http://wiki.netbeans.org/Scala
Bug reports are welcome.
=== Important Notice ===:
If you have a previous Scala plugin installed, after new plugins installed, you need to locate the NetBeans user profile:
- On Windows system, the default location is: "C:\Documents and Settings\<username>\.netbeans\<version-number>\"
- On Unix/Linux/MaxOS system: "<username>/.netbeans/<version-number>/"
=====================
Async or Sync Log in Erlang - Limit the Load of Singleton Process
In a previous blog: A Case Study of Scalability Related "Out of memory" Crash in Erlang, I described a scalability related issue, which was coming from a singleton async logger process. A singleton Erlang process can not benefit from multiple-core scalability.
I then did some testing on disk_log, which fortunately has sync log functions: log/2 and blog/2. Whenever a process calls disk_log:blog/2, the requesting process will monitor the logger process until it returns a result or failure. The piece of code is like:
monitor_request(Pid, Req) -> Ref = erlang:monitor(process, Pid), Pid ! {self(), Req}, receive {'DOWN', Ref, process, Pid, _Info} -> {error, no_such_log}; {disk_log, Pid, Reply} -> erlang:demonitor(Ref), receive {'DOWN', Ref, process, Pid, _Reason} -> Reply after 0 -> Reply end end.
Where Pid is the logger's process id.
This piece of code shows how to interactive synchronously between processes.
Under sync mode, there may be a lot of simultaneous requesting processes request the logger process to log message asynchronously, but each requesting process will wait the logger's work done before it requests next log, i.e. each requesting process requests the logger to log message synchronously. Upon this sync mode, we can guarantee the logger's message queue length won't exceed the number of simultaneous requesting processes.
I wrote some testing code: dlogger.erl
-module(dlogger). -export([sync/2, async/2, proc_loop/4 ]). -define(LogName, blog). -define(LogFile, "b.log"). % 100000, 10 sync(N_Msg, N_Procs) -> test(N_Msg, N_Procs, fun disk_log:blog/2). async(N_Msg, N_Procs) -> test(N_Msg, N_Procs, fun disk_log:balog/2). test(N_Msg, N_Procs, FunLog) -> MsgPerProc = round(N_Msg / N_Procs), Collector = init(N_Procs), LogPid = logger_pid(?LogName), io:format("logger pid: ~p~n", [LogPid]), Workers = [spawn(?MODULE, proc_loop, [Collector, MsgPerProc, LogPid, FunLog]) || _I <- lists:seq(1, N_Procs)], Start = now(), [Worker ! start || Worker <- Workers], %% don't terminate, wait here, until all tasks done. receive {sent_done, MaxMQLen, MaxMem} -> probe_logger(LogPid, MaxMQLen, MaxMem), [exit(Worker, kill) || Worker <- Workers], io:format("Time: ~10.2f ms~n", [timer:now_diff(now(), Start) / 1000]) end. init(N_Procs) -> disk_log:close(?LogName), disk_log:open([{name, ?LogName}, {file, ?LogFile}, {format, external}]), MainPid = self(), Collector = spawn(fun() -> collect(MainPid, N_Procs, 0, 0, 0, 0) end), Collector. collect(MainPid, N_Procs, N_Finished, _N_Msg, MaxMQLen, MaxMem) when N_Procs == N_Finished -> MainPid ! {sent_done, MaxMQLen, MaxMem}; collect(MainPid, N_Procs, N_Finished, N_Msg, MaxMQLen, MaxMem) -> receive {Pid, sent_done, MQLen, _Mem} -> io:format("==== QLen ~p. Proc ~p finished, total finished: ~p ====~n", [MQLen, Pid, N_Finished + 1]), collect(MainPid, N_Procs, N_Finished + 1, N_Msg, MaxMQLen, MaxMem); {Pid, I, MQLen, Mem} -> %io:format("Processed/Qlen ~p/~p msgs. Logger mem is ~p. proc ~p: No.~p msgs sent~n", [N_Msg + 1, MQLen, Mem, Pid, I]), MaxMQLen1 = if MQLen > MaxMQLen -> MQLen; true -> MaxMQLen end, MaxMem1 = if Mem > MaxMem -> Mem; true -> MaxMem end, collect(MainPid, N_Procs, N_Finished, N_Msg + 1, MaxMQLen1, MaxMem1) end. proc_loop(Collector, N_Msg, LogPid, LogFun) -> receive start -> do_proc_work(Collector, N_Msg, LogPid, LogFun, do_log) end. do_proc_work(Collector, I, LogPid, LogFun, WorkType) -> Date = httpd_util:rfc1123_date(calendar:local_time()), MQLen = logger_mqlen(LogPid), Mem = logger_mem(LogPid), Msg = io_lib:format("logged in ~p, logger qlen is ~p, total mem is ~p\n", [self(), MQLen, Mem]), Msg1 = list_to_binary([<<"=INFO REPORT==== ">>, Date, <<" ===\n">>, Msg, <<"\n">>]), WorkType1 = if WorkType == do_log -> LogFun(?LogName, Msg1), io:format("", []), % sync the io between collector if any Collector ! {self(), I, MQLen, Mem}, io:format("sent one msg, qlen:~p, mem:~p~n", [MQLen, Mem]), if I =< 1 -> Collector ! {self(), sent_done, MQLen, Mem}, io:format("~p sent done, qlen:~p, mem:~p~n", [self(), MQLen, Mem]), keep_live; true -> do_log end; true -> keep_live end, do_proc_work(Collector, I - 1, LogPid, LogFun, WorkType1). probe_logger(Pid, MaxMQLen, MaxMem) -> MQLen = logger_mqlen(Pid), Mem = logger_mem(Pid), MaxMQLen1 = if MQLen > MaxMQLen -> MQLen; true -> MaxMQLen end, MaxMem1 = if Mem > MaxMem -> Mem; true -> MaxMem end, io:format("qlen is ~p, max qlen is ~p, max mem is ~p~n", [MQLen, MaxMQLen1, MaxMem1]), if MQLen == 0 -> done; true -> timer:sleep(10), probe_logger(Pid, MaxMQLen, MaxMem) end. %% === helper === logger_pid(Log) -> case disk_log_server:get_log_pids(Log) of undefined -> undefined; {local, Pid} -> Pid; {distributed, [Pid|_Pids]} -> Pid end. logger_mqlen(undefined) -> 0; logger_mqlen(Pid) -> case process_info(Pid, message_queue_len) of {message_queue_len, Val} when is_integer(Val) -> Val; _ -> 0 end. logger_mem(undefined) -> 0; logger_mem(Pid) -> case process_info(Pid, memory) of {memory, Val} when is_integer(Val) -> Val; _ -> 0 end.
You can always use process_info/2 or process_info/1 to probe the information of a process. In above code, we will probe the logger process's message queue length and memory via logger_mqlen/1 and logger_mem/1 when necessary.
I wrote the code as it, so I can create thousands of processes first, then each process will repeatedly request to log message MsgPerProc? times, and keep alive after all messages have been sent.
And, to evaluate the actual task time, when all requesting processes have finished sending log messages, a probe_logger/1 function will confirm all messages in logger queue have been processed.
Here's the result:
> dlogger:sync(1000000, 1000) % 1 million log messages under 1000 requesting processes: qlen is 0, max qlen is 999, max mem is 690,400 Time: 861286.24 ms
> dlogger:async(1000000, 1000) % 1 million log messages under 1000 requesting processes: qlen is 0, max qlen is 68487, max mem is 75,830,616 Time: 2156351.45 ms
The performance in async mode is getting much worse comparing to sync mode. Under async mode, not only the elapsed time grew a lot, but also the max queue length reached to 68487, and the memory of logger process reached about 72M. Actually, after all messages had been sent, only a few messages had been processed by logger process. Since I kept 1000 processes alive, the logger process only <b>shared very very poor proportion CPU cycles</b>, the message processing procedure (log to disk buffer here) became very very slow, which caused the worse elapsed time. This case can be applied on any singleton process.
On the other side, under sync mode, the max queue length did not exceed the number of simultaneous requesting processes, here is 999 in case of 1000 simultaneous processes, and the max memory of logger process is reasonable in about 674K.
I'd like to emphasize the points:
- Singleton process in Erlang can not benefit from multiple-core scalability.
- Move code/job out of singleton process as much as possible to simultaneous processes, for example, before send the message to singleton process, done all pre-formatting/computing job.
- Some times, you need to sync the interacting between processes, to limit the singleton process working load.
A Case Study of Scalability Related "Out of memory" Crash in Erlang
We are building a platform for message switching, in Erlang. Everything looks OK on stability and features. It actually has run more than half year with zero down. We tested its performance on our 2-core CPU machine before, and got about 140 transactions/second, it's good enough.
Then, we got a 8-core CPU machine several weeks ago, and we did same performance testing on it, to see the scalability. Since Erlang is almost perfect on scalability, you can image the result, yes, about 700 transactions/second now, scaled almost linear. Until it crashed with "out of memory" when million hits processed.
It left a very big "erl_crash.dump" file there, I had to dig the issue. My first guess was, were some remote requests (access db, access remote web service etc) timeout but the process itself was not timeout yet, and cause more and more processes kept in VM?
A quick grep "=proc:" erl_crash.dump showed that the total number of processes was about 980, which was reasonable for our case.
So, which process ate so many memory? A quick grep "Stack+head" erl_crash.dump showed that there was indeed a process with 285082125 size of Stack+head there.
Following this clue, I caught this process:
=proc:<0.4.0> State: Garbing Name: error_logger Spawned as: proc_lib:init_p/5 Last scheduled in for: io_lib_format:pad_char/2 Spawned by: <0.1.0> Started: Sun Apr 1 01:21:50 2012 Message queue length: 2086029 Number of heap fragments: 1234053 Heap fragment data: 281266956 Link list: [<0.27.0>, <0.0.0>, {from,<0.42.0>,#Ref<0.0.0.88>}] Reductions: 72745575 Stack+heap: 285082125 OldHeap: 47828850 Heap unused: 121777661 OldHeap unused: 47828850 Program counter: 0x0764c66c (io_lib_format:pad_char/2 + 4) CP: 0x0764c1b4 (io_lib_format:collect_cseq/2 + 124)
This process was error_logger, which is from OTP/Erlang standard lib: error_logger, writing received messages to log file or tty. The typical usage is:
error_logger:info_msg("~p:~p " ++ Format, [?MODULE, ?LINE] ++ Data))
Which will format Data to a String according to the Format string, and write it to tty or log file.
he above case showed the message queue length of process "error_logger" had reached 1234053, and the Stack+heap was 285082125, about 272M size.
So the cause may be, that the message queue could not be processed in time, the messages were crowded in error_logger's process and finally caused "out of memory". The bottle-neck was that when error_logger tried to format the message to String, Erlang VM was weak on processing them, which seemed to need a lot of CPU cycles. In my previous blog, I talked about Erlang is bad on massive text processing. Erlang processes String/Text via List, which is obvious bottle-neck in Erlang now, with Erlang is getting much and much popular and more and more Erlang applications are written.
But, why this did not happen on our 2-core CPU machine? It's an interesting scalability related problem:
"error_logger" module will registered one and only one process to receive and handle all log messages. But Erlang VM's scheduler can not distribute ONE process to use multiple CPUs' computing ability. In our 2-core machine, the whole ability is about 140 transactions/second, the one process of "error_logger" just happened to have the power to handle corresponding log messages in time. Under 8-core CPUs machine, our platform scales to handle 700 transactions/second, but there is still only one process of "error_logger", which can not use 8-core CPUs' ability at all, and finally fail on it.
Erlang treats every process fairly (although you can change the priority manually), we can do a simple/quick evaluation:
- 2-Core machine, keeping hits at 140 trans/second:
The number of simultaneous processes will be about 200, each process shares the CPU cycles: 1/200 * 2 Core = 1%
- 8-Core machine, keeping hits at 700 trans/second:
The number of simultaneous processes will be about 980, each process shares the CPU cycles: 1/980 * 8 Core = 0.82%
So, the CPU cycles shared by error_logger process actually not increases. BTW, I think error_logger should cut its message queue when can not process them in time (disk IO may also be slower than receiving messages).
The Year That Will Be
1==0.999999999......
I met Erlang 2 years ago, which finally brings me to Scala. I learnt a lot from Erlang, and I entered the Scala world with Erlang atmosphere surrounding me. The FP, the Pattern Match, the Actor/Process, I found these familiar friends in Scala everywhere.Scala has extra bonus, to me, static types and OO/FP. The domains I face are usually with a lot of business logic, or, the worlds I try to describe are not only messages, they are, models I don't think are suitable to describe in Function only.
The world itself is OO/FP mixed, like Martin's quote: Two sides of coin. It's something like the Particle/Wave in Quantum. The world is an infinite whole, but the reason of Human Being is always finite, we are using out finite reason to measure the infinite world, it's an unsolvable contradiction: Infinity vs Finite. We have to read our world in OO and, in FP, in snapshot and in continuation.
There won't be "Super Hero" in computer languages, the world is getting self-organization and harmony, so do the languages. Each language is living in an eco-system, born, growing via interacting with environment, disappear ...
The Economy
It was bad in 2008. I tried to do some computing on stock market based on my neural network. What I can say is it will be swing in the next half-year, no big drop, no big rise. The Shanghai Stock Index will swing between 1200 and 3000. At least, no big worse any more.My Self
I need to make some big decisions in this a year.
CN Erlounge III
I met Jackyz who is one of the translators of Chinese version "Programming Erlang". And Aimin who is writing a Delphi module to support Erlang c-node and c-driver in Delphi.
There is a commercial network monitoring product using Erlang from a major telecom company in China. And our Mobile-Banking platform (in Erlang) is scheduled to launch at middle of January too.
I talked with Yeka and Diuera from Broadview, a leading publisher in IT in China, they are really interested in importing "Programming in Scala" to mainland China.
And many thanks to Shiwei Xu, who is heavy working on Erlang community in China, and took the place to organize this conference.
I gave some encouragements to younger developers on learning Erlang and reading "Programming Erlang", since I'm the oldest one in attendees :-). Erlang is one of the best pragmatic and clear languages to learn concurrent/parallel and functional programming, and the book, is a very thoughtful and philosophic one on these perceptions.
And I'd like to see "Programming in Scala" also appeals in China soon, Scala is another pragmatic language on solving real world problems and, the book, is also thoughtful and philosophic one on our real world on Types, OO and FP.
Of course, choosing Scala or Erlang for your real world project should depend on the requirements.
I may be back to Vancouver next month for a while. Oh, it will be the beginning of new year.
CN Erlounge III photos by krzycube
Thinking in Scala vs Erlang
Keeping Erlang in mind, I've coded two months in Scala, I'm thinking something called "Scala vs Erlang", I wrote some benchmark code to prove me (the code and result may be available someday), and I'd like to do some gradually summary on it in practical aspect. These opinions may be or not be correct currently due to lacking of deep experience and understanding, but, anyway, I need to record them now and correct myself with more experiences and understanding got on both Scala and Erlang.
Part I. Syntax
Keeping Erlang in mind, I've coded two months in Scala, I'm thinking something called "Scala vs Erlang", I wrote some benchmark code to prove me (the code and result may be available someday), and I'd like to do some gradually summary on it in practical aspect. These opinions may be or not be correct currently due to lacking of deep experience and understanding, but, anyway, I need to record them now and correct myself with more experiences and understanding got on both Scala and Erlang.
Part I. Syntax
List comprehension
Erlang:
Lst = [1,2,3,4], [X + 1 || X <- Lst], lists:map(fun(X) -> X + 1 end, Lst)
Scala:
val lst = List(1,2,3,4) for (x <- lst) yield x + 1 lst.map{x => x + 1} lst.map{_ + 1} // or place holder
Pattern match
Erlang:
case X of {A, B} when is_integer(A), A > 1 -> ok; _ -> error end, {ok, [{A, B} = H|T]} = my_function(X)
Scala:
x match { case (a:Int, b:_) if a > 1 => OK // can match type case _ => ERROR } val ("ok", (h@(a, b)) :: t) = my_function(x)
List, Tuple, Array, Map, Binary, Bit
Erlang:
Lst = [1, 2, 3] %% List [0 | Lst] %% List concat {1, 2, 3} %% Tuple <<1, 2, "abc">> %% Binary %% no Array, Map syntax
Scala:
val lst = List(1, 2, 3) // List 0 :: lst // List concat (1, 2, 3) // Tuple Array(1, 2, 3) // Array Map("a" -> 1, "b" -> 2) // Map // no Binary, Bit syntax
Process, Actor
Erlang:
the_actor(X) -> receive ok -> io:format("~p~n", [X]); I -> the_actor(X + I) %% needs to explicitly continue loop end. P = spawn(mymodule, the_actor, [0]) P ! 1 P ! ok
Scala I:
class TheActor(x:Int) extends Actor { def act = loop { react { case "ok" => println(x); exit // needs to explicitly exit loop case i:Int => x += i } } } val a = new TheActor(0) a ! 1 a ! "ok"
Scala II:
val a = actor { def loop(x:Int) = { react { case "ok" => println(x) case i:Int => loop(x + i) } } loop(0) } a ! 1 a ! "ok"
Part II. Processes vs Actors
Something I
Erlang:
- Lightweight processes
- You can always (almost) create a new process for each new comer
- Scheduler treats all processes fairly
- Share nothing between processes
- Lightweight context switch between processes
- IO has been carefully delegated to independent processes
Scala:
- Active actor is delegated to JVM thread, actor /= thread
- You can create a new actor for each new comer
- But the amount of real workers (threads) is dynamically adjusted according to the processing time
- The later comers may be in wait list for further processing until a spare thread is available
- Share nothing or share something upon you decision
- Heavy context switch between working threads
- IO block is still pain unless good NIO framework (Grizzly?)
Something II
Erlang:
- Try to service everyone simultaneously
- But may loss service quality when the work is heavy, may time out (out of service)
- Ideal when processing cost is comparable to context switching cost
- Ideal for small message processing in soft real-time
- Bad for massive data processing, and cpu-heavy work
Scala:
- Try to service limited number of customers best first
- If can not service all, the later comers will be put in waiting list and may time out (out of service)
- It's difficult for soft real-time on all coming concurrent customers
- Ideal when processing cost is far more than context switching cost (context switch time is in μs on modern JVM)
- When will there be perfect NIO + Actor library?
Erlang Plugin for NetBeans - 0.17.0 Released
I'm pleased to announce Erlang plugin for NetBeans (ErlyBird) 0.17.0 is released.
This is a bug-fix release, and from now on, will be in form of NetBeans plugin.
NetBeans 6.5 is a requirement.
To download, please go to: https://sourceforge.net/project/showfiles.php?group_id=192439&package_id=226387&release_id=642911
To install:
- Open NetBeans, go to "Tools" -> "Plugins", click on "Downloaded" tab title, click on "Add Plugins..." button, choose the directory where the Erlang plugin are unzipped, select all listed *.nbm files, following the instructions. Restart IDE.
- Check/set your OTP path. From [Tools]->[Options], click on 'Miscellanous', then expand 'Erlang Installation', fill in the full path of your 'erl.exe' or 'erl' file. For instance: "C:/erl/bin/erl.exe"
When you open/create an Erlang project first time, the OTP libs will be indexed. The indexing time varies from 30 to 60 minutes depending on your computer.
Feedback and bug reports are welcome.
NetBeans on OpenSolaris 08.11 in VirtualBox in Mac OS
My Macbook is ân old one with Mac OS X 10.4, I have no way to get Java 6. I´ve tracked OpenSolaris? for a long time, with OpenSolaris? 08.11 is going to be released, I think I should have a try to see if I can do my daily work on OpenSolaris? rather than Mac OS on my MacBook?.
I then installed VirtualBox? on my MacOS, then a guest OpenSolaris? with 1024M memory and 16G disk. I downloaded and installed Java JDK 6 and NetBeans 6.5, plus my Scala plugins.
It rocks seamlessly. The only problem is, should I re-format my harddisk and install OpenSolaris? instead of Mac OS now?
There are a lot of guesses on Sun´s future these days, but, with all those innovations from Sun or taken by Sun, Why cloud-computing? Should still be sunshine-computing.
Click on the picture to enlarge it
Scala Plugin for Coming NetBeans 6.5 Official Release
- Much better code-completion
- Two new color themes: Twilight and Emacs Standard
- Various bugs fixes
- It's not perfect, but fairly stable
- Works good with NetBeans Maven plugin
To download, please go to: https://sourceforge.net/project/showfiles.php?group_id=192439&package_id=256544&release_id=641359
For more information, please see http://wiki.netbeans.org/Scala
Bug reports are welcome.
It works also on NetBeans RC2. If you have previous version of Scala plugin installed, you can upgrade to this version.
Scala for NetBeans Screenshot#15: Twilight Color Theme
>>> Updated Nov 11:
Emacs Standard color theme already be there.
======
I'm beginning to write some real thing based on Liftweb. The more code I wrote, the more bugs of Scala plugin were fixed.
Not only bugs are being fixed, I also created a Twilight color theme for this plugin. And an Emacs color theme is also on the road.
When NetBeans 6.5 is official released, I'll put a new Scala plugin too.
Click on the picture to enlarge it
New Scala Plugin for NetBeans 6.5 RC2 Is Packed
I packed a new Scala plugin, and tried several times to upload it to NetBeans' Plugins Portal and could not get job successfully done. So I uploaded it to sourceforge.net instead. The zip file is located at: [ https://sourceforge.net/project/showfiles.php?group_id=192439&package_id=256544&release_id=638797 scala-plugin-081106.zip
NetBeans' trunk has been targeting 7.0 now, the plugin on Last Development Build update center is no longer compatible with 6.5 RC2. That is, you should ignore my previous blog talked about installing plugin on 6.5 RC2, if you are using 6.5 RC2, you should download and install this one.
Nightly built version users can get latest plugin via NetBeans' plugin management feature as normal.
For more information, please visit http://wiki.netbeans.org/Scala
This version fixed various bugs, and with some enhancements. The bundling Scala runtime is 2.7.2RC6, with NetBeans' maven plugin, it works on newest Liftweb project.
Bug reports are always welcome.
Click on the picture to enlarge it
Install Scala Plugin for NetBeans 6.5 RC2
Content of this blog is out of date, please look at New Scala Plugin for NetBeans 6.5 RC2 Is Packed
===
NetBeans 6.5 RC2 released. The Scala plugin on NetBeans' Plugins Portal is not compilable with RC2. To get Scala plugin working with RC2, do:
# Open NetBeans 6.5 RC2, go to "Tools" -> "Plugins", check "Setting" -> "Add", add new update center as "Last Development Build" with url: http://deadlock.netbeans.org/hudson/job/nbms-and-javadoc/lastStableBuild/artifact/nbbuild/nbms/updates.xml.gz
# Then in the "Available Plugins" tab, you can find the "Scala" category (or, you can click on "Name" in "Available Plugins" tab to find them. You may need to click "Reload Catalog" to get the latest available modules), check "Scala Kit" and click "Install", following the instructions. Restart IDE.
I'll re-pack a new version of Scala plugin for Plugins Portal when NetBeans 6.5 is officially released.
Scala for NetBeans Screenshot#14: Refined Color Theme
With more Scala coding experience, I refined color theme of Scala plugin for NetBeans. By desalinating Type name a bit, I found I can concentrate on logic a bit better. And all function calls are highlighted now, so, for multiple-line expression, when error-prone op is put on wrong new line, you can get some hints at once. It also gives you hint if a val/var is with implicit calling, which will be highlighted as a function call.
There are still some bugs when infer var/val's correct type in some cases.
Now the editor is much informative with highlighting and bubble pop display of type information (move mouse on it with CTRL/COMMAND pressed).
You'll need to update to newest 1.9.0 Editing module, please wait for it appealing on Development Update Center.
Click on the picture to enlarge it
RPC Server for Erlang, In Scala
There has been Java code in my previous blog: RPC Server for Erlang, In Java, I'm now try to rewrite it in Scala. With the pattern match that I've been familiar with in Erlang, write the Scala version is really a pleasure. You can compare it with the Java version.
I do not try Scala's actor lib yet, maybe late.
And also, I should port Erlang's jinterface to Scala, since OtpErlangTuple?, OtpErlangList? should be written in Scala's Tuple and List.
The code is auto-formatted by NetBeans' Scala plugin, and the syntax highlighting is the same as in NetBeans, oh, not exactly.
/* * RpcMsg.scala * */ package net.lightpole.rpcnode import com.ericsson.otp.erlang.{OtpErlangAtom, OtpErlangList, OtpErlangObject, OtpErlangPid, OtpErlangRef, OtpErlangTuple} class RpcMsg(val call:OtpErlangAtom, val mod :OtpErlangAtom, val fun :OtpErlangAtom, val args:OtpErlangList, val user:OtpErlangPid, val to :OtpErlangPid, val tag :OtpErlangRef) { } object RpcMsg { def apply(msg:OtpErlangObject) : Option[RpcMsg] = msg match { case tMsg:OtpErlangTuple => tMsg.elements() match { /* {'$gen_call', {To, Tag}, {call, Mod, Fun, Args, User}} */ case Array(head:OtpErlangAtom, from:OtpErlangTuple, request:OtpErlangTuple) => if (head.atomValue.equals("$gen_call")) { (from.elements, request.elements) match { case (Array(to :OtpErlangPid, tag:OtpErlangRef), Array(call:OtpErlangAtom, mod :OtpErlangAtom, fun :OtpErlangAtom, args:OtpErlangList, user:OtpErlangPid)) => if (call.atomValue.equals("call")) { Some(new RpcMsg(call, mod, fun, args, user, to, tag)) } else None case _ => None } } else None case _ => None } case _ => None } }
/* * RpcNode.scala * * To change this template, choose Tools | Template Manager * and open the template in the editor. */ package net.lightpole.rpcnode import com.ericsson.otp.erlang.{OtpAuthException, OtpConnection, OtpErlangAtom, OtpErlangExit, OtpErlangObject, OtpErlangString, OtpErlangTuple, OtpSelf} import java.io.IOException import java.net.InetAddress import java.net.UnknownHostException import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import java.util.logging.Level import java.util.logging.Logger trait Cons { val OK = new OtpErlangAtom("ok") val ERROR = new OtpErlangAtom("error") val STOPED = new OtpErlangAtom("stoped") val THREAD_POOL_SIZE = 100 } /** * * Usage: * $ erl -sname clientnode -setcookie mycookie * (clientnode@cmac)> rpc:call(xnodename@cmac, xnode, parse, []). * * @author Caoyuan Deng */ abstract class RpcNode(xnodeName:String, cookie:String, threadPoolSize:Int) extends Cons { def this(xnodeName:String, cookie:String) = this(xnodeName, cookie, 100) private var xSelf:OtpSelf = _ private var sConnection:OtpConnection = _ private var execService:ExecutorService = Executors.newFixedThreadPool(threadPoolSize) private val flags = Array(0) startServerConnection(xnodeName, cookie) loop def startServerConnection(xnodeName:String, cookie:String ) = { try { xSelf = new OtpSelf(xnodeName, cookie); // The node then publishes its port to the Erlang Port Mapper Daemon. // This registers the node name and port, making it available to a remote client process. // When the port is published it is important to immediately invoke the accept method. // Forgetting to accept a connection after publishing the port would be the programmatic // equivalent of false advertising val registered = xSelf.publishPort(); if (registered) { System.out.println(xSelf.node() + " is ready."); /** * Accept an incoming connection from a remote node. A call to this * method will block until an incoming connection is at least * attempted. */ sConnection = xSelf.accept(); } else { System.out.println("There should be an epmd running, start an epmd by running 'erl'."); } } catch { case ex:IOException => case ex:OtpAuthException => } } def loop : Unit = { try { val msg = sConnection.receive val task = new Runnable() { override def run = RpcMsg(msg) match { case None => try { sConnection.send(sConnection.peer.node, new OtpErlangString("unknown request")); } catch { case ex:IOException => } case Some(call) => val t0 = System.currentTimeMillis flag(0) = processRpcCall(call) System.out.println("Rpc time: " + (System.currentTimeMillis() - t0) / 1000.0) } } execService.execute(task) if (flag(0) == -1) { System.out.println("Exited") } else loop } catch { case ex:IOException => loop case ex:OtpErlangExit => case ex:OtpAuthException => } } /** @throws IOException */ def sendRpcResult(call:RpcMsg, head:OtpErlangAtom, result:OtpErlangObject) = { val tResult = new OtpErlangTuple(Array(head, result)) // Should specify call.tag here val msg = new OtpErlangTuple(Array(call.tag, tResult)) // Should specify call.to here sConnection.send(call.to, msg, 1024 * 1024 * 10) } /** @abstact */ def processRpcCall(call:RpcMsg) : Int } object RpcCall { def getShortLocalHost : String = getLocalHost(false) def getLongLocalHost : String = getLocalHost(true) def getLocalHost(longName:Boolean) : String = { var localHost = "localhost" try { localHost = InetAddress.getLocalHost.getHostName; if (!longName) { /* Make sure it's a short name, i.e. strip of everything after first '.' */ val dot = localHost.indexOf(".") if (dot != -1) localHost = localHost.substring(0, dot) } } catch { case ex:UnknownHostException => } localHost } }
FOR, WHILE Is Too Easy, Let's Go Looping
With several 10k code in Erlang, I'm familiar with functional style coding, and I found I can almost rewrite any functions in Erlang to Scala, in syntax meaning.
Now, I have some piece of code written in Java, which I need to translate them to Scala. Since "for", "while", or "do" statement is so easy in Java, I can find a lot of them in Java code. The problem is, should I keep them in the corresponding "for", "while", "do" in Scala, or, as what I do in Erlang, use recursive function call, or, "loop"?
I sure choose to loop, and since Scala supports recursive function call on functions defined in function body (Erlang doesn't), I choose define these functions' name as "loop", and I tried to write code let "loop" looks like a replacement of "for", "while" etc.
Here's a piece of code that is used to read number string and convert to double, only piece of them.
The Java code:
public class ReadNum { private double readNumber(int fstChar, boolean isNeg) { StringBuilder out = new StringBuilder(22); out.append(fstChar); double v = '0' - fstChar; // the maxima length of number stirng won't exceed 22 for (int i = 0; i < 22; i++) { int c = getChar(); switch (c) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': v = v * 10 - (c - '0'); out.append(c); continue; case '.': out.append('.'); return readFrac(out, 22 - i); case 'e': case 'E': out.append(c); return readExp(out, 22 - i); default: if (c != -1) backup(1); if (!isNeg) return v; else return -v } } return 0; } }
The Scala code:
class ReadNum { private def readNumber(fstChar:Char, isNeg:Boolean) :Double = { val out = new StringBuilder(22) out.append(fstChar) val v:Double = '0' - fstChar def loop(c:Char, v:Double, i:Int) :Double = c match { // the maxima length of number stirng won't exceed 22 case _ if i > 21 => 0 case '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' => out.append(c) val v1 = v * 10 - (c - '0') loop(getChar, v1, i + 1) case '.' => out.append('.') readFrac(out, 22 - i) case 'e' | 'E' => out.append(c) readExp(out, 22 - i) case _ => if (c != -1) backup(1) if (isNeg) v else -v }; loop(getChar, v, 1) } }
As you can see in line 25, the loop call is put at the position immediately after the "loop" definition, following "}; ", I don't put it to another new line, it makes me aware of the "loop" function is just used for this call.
And yes, I named all these embedded looping function as "loop", every where.
An Example Syntax in Haskell, Erlang and Scala
>>> Updated Oct 16:
I found some conventions of coding style make code more readable for Scala. For example, use <pre>{ x => something } instead of (x => dosomething)</pre> <p>for anonymous function; Use x, y, z as the names of arguments of anonymous functions; Put all modifiers to the ahead line etc. That makes me can pick up these functions by eyes quickly.</br>
======
It's actually my first time to write true Scala code, sounds strange? Before I write Scala code, I wrote a Scala IDE first, and am a bit familiar with Scala syntax now. And I've got about 1.5 year experience on Erlang, it began after I wrote ErlyBird.
Now it's time to write some real world Scala code, I choose to port Paul R. Brown's perpubplat blog engine, which is written in Haskell. And I have also some curiosities on how the syntax looks in Erlang, so I tried some Erlang code too.
Here's some code piece of entry module in Haskell, Erlang and Scala:
Original Haskell code piece
empty :: Model empty = Model M.empty M.empty M.empty [] 0 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 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)
In Erlang:
% @spec empty :: Model empty() -> #model{}. % @spec build_model :: [Item] -> Model build_model([]) -> empty(); build_model(Is) -> SortedIs = sort_by_created_reverse(Is), Bid = dict:from_list([{I#item.internal_id, I} || I <- SortedIs]), N = lists:max(dict:fetch_keys(Bid)), #model{by_permatitle = dict:from_list([{X#item.permatitle, X} || X <- SortedIs]), by_int_id = Bid, child_map = build_child_map(SortedIs), all_items = SortedIs, next_id = N + 1}. % @spec build_child_map :: [Item] -> M.Map Int [Int] build_child_map(Is) -> build_child_map_(dict:from_list(lists:map(fun (X) -> {X#item.internal_id, []} end), Is), Is). %% Constructed to take advantage of the input being in sorted order. % @spec build_child_map_ :: M.Map Int [Int] -> [Item] -> M.Map Int [Int] build_child_map_(D, []) -> D; build_child_map_(D, [I|Is]) -> case I#item.parent of undefined -> build_child_map_(D, Is); P_Id -> build_child_map_(dict:append(unwrap(P_Id), I#item.internal_id, D), Is) end. % @spec sort_by_created_reverse :: [Item] -> [Item] sort_by_created_reverse(Is) -> lists:sort(fun created_sort_reverse/2, Is). % @spec created_sort_reverse :: Item -> Item -> Ordering created_sort_reverse(A, B) -> compare(B#item.created, A#item.created).
In Scala
object Entry { def empty = new Model() def build_model(is:List[Item]) = is match { case Nil => empty case _ => val sortedIs = sortByCreatedReverse(is) val bid = Map() ++ sortedIs.map{ x => (x.internalId -> x) } val n = bid.keys.toList.sort{ (x, y) => x > y }.head // 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{ x => (x.internalId -> Nil) }, is) private def buildChildMap_(map:Map[Int, List[Int]], is:List[Item]) = { map ++ (for (i <- is if i.parent.isDefined; pid = i.parent.get; cids = map.getOrElse(pid, Nil)) yield (pid -> (cids + i.internalId))) } def sortByCreatedReverse(is:List[Item]) = is.sort{ (x, y) => x.created before y.created } }
>>> Updated Oct 16:
Per Martin's suggestion, the above code can be written more Scala style (the reasons are in the comments). Thanks, Martin.
object Entry { def empty = new Model() def build_model(is:List[Item]) = is match { case Nil => empty case _ => val sortedIs = sortByCreatedReverse(is) val bid = Map() ++ sortedIs.map{ x => (x.internalId -> x) } // use predefined max in Iterable val n = Iterable.max(bid.keys.toList) new Model(Map() ++ sortedIs.map{ x => (x.permatitle -> x) }, bid, buildChildMap(sortedIs), sortedIs, n + 1) } // you can use a wildcard anonymousfunction here def buildChildMap(is:List[Item]) = buildChildMap_(Map() ++ is.map(_.internalId -> Nil), is) private def buildChildMap_(map:Map[Int, List[Int]], is:List[Item]) = map ++ { // rewrite for so that definitions go into body -- it's more efficient. for (i <- is if i.parent.isDefined) yield { val pid = i.parent.get val cids = map.getOrElse(pid, Nil) pid -> (cids + i.internalId) } } // you can use a wildcard anonymous function here def sortByCreatedReverse(is:List[Item]) = is.sort{ _.created before _.created } }
======
I use ErlyBird for Erlang coding, and Scala for NetBeans for Scala coding. The experience seems that IDE is much aware of Scala, and I can get the typing a bit faster than writing Erlang code.
If you are not familiar with all these 3 languages, which one looks more understandable?
RPC Server for Erlang, In Java
We are using Erlang to do some serious things, one of them is indeed part of a banking system. Erlang is a perfect language in concurrent and syntax (yes, I like its syntax), but lacks static typing (I hope new added -spec and -type attributes may be a bit helping), and, is not suitable for processing massive data (performance, memory etc). I tried parsing a 10M size XML file with xmerl, the lib for XML in OTP/Erlang, which causes terrible memory disk-swap and I can never get the parsed tree out.
It's really a need to get some massive data processed in other languages, for example, C, Java etc. That's why I tried to write RPC server for Erlang, in Java.
There is a jinterface lib with OTP/Erlang, which is for communication between Erlang and Java. And there are docs for how to get it to work. But, for a RPC server that is called from Erlang, there are still some tips for real world:
1. When you send back the result to caller, you need set the result as a tuple, with caller's tag Ref as the first element, and the destination should be the caller's Pid. It's something like:
OtpErlangTuple msg = new OtpErlangTuple(new OtpErlangObject[] {call.tag, tResult}); sConnection.send(call.to, msg);
where, call.tag is a OtpErlangRef?, and tResult can be any OtpErlangObject?, call.to is a OtpErlangPid?.
2. If you need to send back a massive data back to caller, the default buffer size of OtpErlangOutputStream? is not good, I set it to 1024 * 1024 * 10
3. Since there may be a lot of concurrent callers call your RPC server, you have to consider the concurrent performance of your server, I choose using thread pool here.
The RPC server in Java has two class, RpcNode?.java, and RpcMsg?.java:
package net.lightpole.rpcnode; import com.ericsson.otp.erlang.OtpErlangAtom; import com.ericsson.otp.erlang.OtpErlangList; import com.ericsson.otp.erlang.OtpErlangObject; import com.ericsson.otp.erlang.OtpErlangPid; import com.ericsson.otp.erlang.OtpErlangRef; import com.ericsson.otp.erlang.OtpErlangTuple; /** * * @author Caoyuan Deng */ public class RpcMsg { public OtpErlangAtom call; public OtpErlangAtom mod; public OtpErlangAtom fun; public OtpErlangList args; public OtpErlangPid user; public OtpErlangPid to; public OtpErlangRef tag; public RpcMsg(OtpErlangTuple from, OtpErlangTuple request) throws IllegalArgumentException { if (request.arity() != 5) { throw new IllegalArgumentException("Not a rpc call"); } /* {call, Mod, Fun, Args, userPid} */ if (request.elementAt(0) instanceof OtpErlangAtom && ((OtpErlangAtom) request.elementAt(0)).atomValue().equals("call") && request.elementAt(1) instanceof OtpErlangAtom && request.elementAt(2) instanceof OtpErlangAtom && request.elementAt(3) instanceof OtpErlangList && request.elementAt(4) instanceof OtpErlangPid && from.elementAt(0) instanceof OtpErlangPid && from.elementAt(1) instanceof OtpErlangRef) { call = (OtpErlangAtom) request.elementAt(0); mod = (OtpErlangAtom) request.elementAt(1); fun = (OtpErlangAtom) request.elementAt(2); args = (OtpErlangList) request.elementAt(3); user = (OtpErlangPid) request.elementAt(4); to = (OtpErlangPid) from.elementAt(0); tag = (OtpErlangRef) from.elementAt(1); } else { throw new IllegalArgumentException("Not a rpc call."); } } /* {'$gen_call', {To, Tag}, {call, Mod, Fun, Args, User}} */ public static RpcMsg tryToResolveRcpCall(OtpErlangObject msg) { if (msg instanceof OtpErlangTuple) { OtpErlangTuple tMsg = (OtpErlangTuple) msg; if (tMsg.arity() == 3) { OtpErlangObject[] o = tMsg.elements(); if (o[0] instanceof OtpErlangAtom && ((OtpErlangAtom) o[0]).atomValue().equals("$gen_call") && o[1] instanceof OtpErlangTuple && ((OtpErlangTuple) o[1]).arity() == 2 && o[2] instanceof OtpErlangTuple && ((OtpErlangTuple) o[2]).arity() == 5) { OtpErlangTuple from = (OtpErlangTuple) o[1]; OtpErlangTuple request = (OtpErlangTuple) o[2]; try { return new RpcMsg(from, request); } catch (IllegalArgumentException ex) { ex.printStackTrace(); } } } } return null; } }
package net.lightpole.rpcnode; import com.ericsson.otp.erlang.OtpAuthException; import com.ericsson.otp.erlang.OtpConnection; import com.ericsson.otp.erlang.OtpErlangAtom; import com.ericsson.otp.erlang.OtpErlangExit; import com.ericsson.otp.erlang.OtpErlangObject; import com.ericsson.otp.erlang.OtpErlangString; import com.ericsson.otp.erlang.OtpErlangTuple; import com.ericsson.otp.erlang.OtpSelf; import java.io.IOException; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.logging.Level; import java.util.logging.Logger; /** * * Usage: * $ erl -sname clientnode -setcookie mycookie * (clientnode@cmac)> rpc:call(xnodename@cmac, 'System', currentTimeMillis, []). * * @author Caoyuan Deng */ public abstract class RpcNode { public static final OtpErlangAtom OK = new OtpErlangAtom("ok"); public static final OtpErlangAtom ERROR = new OtpErlangAtom("error"); public static final OtpErlangAtom STOPED = new OtpErlangAtom("stoped"); private static final int THREAD_POOL_SIZE = 100; private OtpSelf xSelf; private OtpConnection sConnection; private ExecutorService execService; public RpcNode(String xnodeName, String cookie) { this(xnodeName, cookie, THREAD_POOL_SIZE); } public RpcNode(String xnodeName, String cookie, int threadPoolSize) { execService = Executors.newFixedThreadPool(threadPoolSize); startServerConnection(xnodeName, cookie); loop(); } private void startServerConnection(String xnodeName, String cookie) { try { xSelf = new OtpSelf(xnodeName, cookie); boolean registered = xSelf.publishPort(); if (registered) { System.out.println(xSelf.node() + " is ready."); /** * Accept an incoming connection from a remote node. A call to this * method will block until an incoming connection is at least * attempted. */ sConnection = xSelf.accept(); } else { System.out.println("There should be an epmd running, start an epmd by running 'erl'."); } } catch (IOException ex) { Logger.getLogger(RpcNode.class.getName()).log(Level.SEVERE, null, ex); } catch (OtpAuthException ex) { Logger.getLogger(RpcNode.class.getName()).log(Level.SEVERE, null, ex); } } private void loop() { while (true) { try { final int[] flag = {0}; final OtpErlangTuple msg = (OtpErlangTuple) sConnection.receive(); Runnable task = new Runnable() { public void run() { RpcMsg call = RpcMsg.tryToResolveRcpCall(msg); if (call != null) { long t0 = System.currentTimeMillis(); flag[0] = processRpcCall(call); System.out.println("Rpc time: " + (System.currentTimeMillis() - t0) / 1000.0); } else { try { sConnection.send(sConnection.peer().node(), new OtpErlangString("unknown request")); } catch (IOException ex) { Logger.getLogger(RpcNode.class.getName()).log(Level.SEVERE, null, ex); } } } }; execService.execute(task); if (flag[0] == -1) { System.out.println("Exited"); break; } } catch (OtpErlangExit ex) { Logger.getLogger(RpcNode.class.getName()).log(Level.SEVERE, null, ex); } catch (IOException ex) { Logger.getLogger(RpcNode.class.getName()).log(Level.SEVERE, null, ex); } catch (OtpAuthException ex) { Logger.getLogger(RpcNode.class.getName()).log(Level.SEVERE, null, ex); } } } protected void sendRpcResult(RpcMsg call, OtpErlangAtom head, OtpErlangObject result) throws IOException { OtpErlangTuple tResult = new OtpErlangTuple(new OtpErlangObject[] {head, result}); // Should specify call.tag here OtpErlangTuple msg = new OtpErlangTuple(new OtpErlangObject[]{call.tag, tResult}); // Should specify call.to here sConnection.send(call.to, msg, 1024 * 1024 * 10); } public abstract int processRpcCall(RpcMsg call); // ------ helper public static String getShortLocalHost() { return getLocalHost(false); } public static String getLongLocalHost() { return getLocalHost(true); } private static String getLocalHost(boolean longName) { String localHost; try { localHost = InetAddress.getLocalHost().getHostName(); if (!longName) { /* Make sure it's a short name, i.e. strip of everything after first '.' */ int dot = localHost.indexOf("."); if (dot != -1) { localHost = localHost.substring(0, dot); } } } catch (UnknownHostException e) { localHost = "localhost"; } return localHost; } }
As you can see, the RpcNode? is an abstract class, by implement int processRpcCall(RpcMsg? call), you can get your what ever wanted features. For example:
package net.lightpole.xmlnode; import basexnode.Main; import com.ericsson.otp.erlang.OtpErlangAtom; import com.ericsson.otp.erlang.OtpErlangList; import com.ericsson.otp.erlang.OtpErlangObject; import com.ericsson.otp.erlang.OtpErlangString; import java.io.IOException; import net.lightpole.rpcnode.RpcMsg; import net.lightpole.rpcnode.RpcNode; /** * * @author dcaoyuan */ public class MyNode extends RpcNode { public MyNode(String xnodeName, String cookie, int threadPoolSize) { super(xnodeName, cookie, threadPoolSize); } @Override public int processRpcCall(RpcMsg call) { final String modStr = call.mod.atomValue(); final String funStr = call.fun.atomValue(); final OtpErlangList args = call.args; try { OtpErlangAtom head = ERROR; OtpErlangObject result = null; if (modStr.equals("xnode") && funStr.equals("stop")) { head = OK; sendRpcResult(call, head, STOPED); return -1; } if (modStr.equals("System") && funStr.equals("currentTimeMillis")) { head = OK; long t = System.currentTimeMillis(); result = new OtpErlangLong(t); } else { result = new OtpErlangString("{undef,{" + modStr + "," + funStr + "}}"); } if (result == null) { result = new OtpErlangAtom("undefined"); } sendRpcResult(call, head, result); } catch (IOException ex) { ex.printStackTrace(); } catch (Exception ex) { } return 0; } }
I tested MyNode? by:
$ erl -sname clientnode -setcookie mycookie ... (clientnode@cmac)> rpc:call(xnodename@cmac, 'System', currentTimeMillis, []).
And you can try to test its concurrent performance by:
%% $ erl -sname clientnode -setcookie mycookie %% > xnode_test:test(10000)
-module(xnode_test). -export([test/1]). test(ProcN) -> Workers = [spawn_worker(self(), fun rpc_parse/1, {}) || I <- lists:seq(0, ProcN - 1)], Results = [wait_result(Worker) || Worker <- Workers]. rpc_parse({}) -> rpc:call(xnodename@cmac, 'System', currentTimeMillis, []). spawn_worker(Parent, F, A) -> erlang:spawn_monitor(fun() -> Parent ! {self(), F(A)} end). wait_result({Pid, Ref}) -> receive {'DOWN', Ref, _, _, normal} -> receive {Pid, Result} -> Result end; {'DOWN', Ref, _, _, Reason} -> exit(Reason) end.
I spawned 10000 calls to it, and it run smoothly.
I'm also considering to write a more general-purpose RPC server in Java, which can dynamically call any existed methods of Java class.
Things To Do in Coming Months
As the beta of Scala for NetBeans released, I found I have several things to do in the coming months.
First, I'll keep the Scala plugins going on, I'll try to re-implement the Project supporting, which, may be an extension of current NetBeans' plain Java Project, that is, you just create plain JSE or JEE project, then add Scala source files to this project, you may mix Java/Scala in one project. Another perception is, it's time to re-write whole things in Scala itself? I have a featured Scala IDE now, or, the chicken, I should make eggs via this chicken instead of duck.
Second, we get some contracts on mobile application for Banking, which, will be implemented via our current Atom/Atom Publish Protocol web service platform. The platform is written in Erlang, but, with more and more business logical requirements, maybe we should consider some Scala things?
Third, oh, it's about AIOTrade, I'v left it at corner for almost one and half year, I said it would be re-written in Scala someday, I really hope I have time. I got some requests to support drawing charts for web application, it actually can, if you understand the source code, I just wrote an example recently, I may post an article on how to do that.
Scala for Netbeans Beta Is Ready, Working with NetBeans 6.5 Beta
>>> Updated Aug 15:
For Windows Vista users: There is a known bug #135547 that may have been fixed in trunk but not for NetBeans 6.5 Beta, which causes exception of "NullPointerException at org.openide.filesystems.FileUtil.normalizeFileOnWindows" when create Scala project. If you are Vista user and like to have a try on Scala plugins, you may need to download a recent nightly build version of NetBeans. Since I have none Vista environment, I'm not sure about above message.
======
I'm pleased to announce that the first beta of Scala for NetBeans is released, followed NetBeans 6.5 beta releasing. The availability and installation instructions can be found at http://wiki.netbeans.org/Scala.
Features:
- Full featured Scala editor
- syntax and semantic coloring
- outline navigator
- code folding
- mark occurrences
- go to declaration
- instant rename
- indentation
- formatting
- pair matching
- error annotations
- code completion
- Project management (build/run/debug project)
- Debugger
- Interactive console
- JUnit integration
- Maven integration (works with [ http://www.liftweb.net Lift Web Framework)
There are some known issues. Bug reports are welcome.
Installation on NetBeans 6.5 beta:
- Get the NetBeans 6.5 beta or later version from: > http://download.netbeans.org/netbeans/6.5/beta/
- Get the Scala plugins beta binary from: http://plugins.netbeans.org/PluginPortal/faces/PluginDetailPage.jsp?pluginid=11854
- Unzip Scala plugin binary to somewhere
- Open NetBeans, go to "Tools" -> "Plugins", click on "Downloaded" tab title, click on "Add Plugins..."
button, choose the directory where the Scala plugins are unzipped, select all listed *.nbm files, following the instructions. Restart IDE.
Run/Debug Lift Web App Using Scala/Maven Plugin for NetBeans
I'm a newbie to Maven, so I encountered some issues when run/debug Lift apps. The following are tips that I got, it may not be perfect, but at least work.
1. When pom.xml is changed, and classes not found errors happen on editor, you can close and reopen the project
This is a known issue, that I didn't refresh compiling context when pom.xml changed, thus the classpath dependency may be not refreshed at once, I'll fix this issue in the near future.
2. Run project by external Maven instead of embedded Maven of NetBeans plugin
I encountered java.lang.NoClassDefFoundError org/codehaus/plexus/util/DirectoryScanner when use embedded NetBeans Maven plugin (3.1.3) when invoke "Run" project, but fortunately, you can custom the binding actions in NetBeans Maven. The steps are:
- Right click project node, choose "Properties"
- Click on "Actions" in left-pane, choose "Use external Maven for build execution", and "set external Maven home"
- Choose "Run project" in right-pane, input "jetty:run"
- Choose "Debug project" in right-pane, input "jetty:run"
3. How to debug project
I'm a bit stupid here, since I don't like to change MAVEN_OPTS frequently. So I choose to do:
$ cd $MAVEN_HOME$/bin $ cp mvn mvn.run $ cp mvnDebug mvn
Then I invoke "Debug" action from NetBeans toolbar, and get NetBeans' output window saying:
WARNING: You are running Maven builds externally, some UI functionality will not be available. Executing:/Users/dcaoyuan/apps/apache-maven-2.0.9/bin/mvn jetty:run Preparing to Execute Maven in Debug Mode Listening for transport dt_socket at address: 8000
Open menu "Debug" -> "Attach Debugger...", in the popped window, for "Port:", input "8000". Everything goes smoothly then. You add/remove breakpoints just as you are doing for a regular Scala project.
Of course, if you want to turn back to "Run" from "Debug", you have to "cp mvn.run mvn" back.
Anybody can give me hints on how to get this setting simple? in NetBeans Maven plugin.
Here's a snapshot: (click to enlarge it)
Scala, NetBeans, Maven, and yes, Lift now
Per recent changes, Scala for NetBeans can live with Maven for NetBeans, and yes, Lift web framework.
The Maven for NetBeans has done an excellent work for Maven's project integration, with proper classpath supporting and indexing. So the Scala editor is well aware of auto-completion, types etc.
Here's a snapshot: (click to enlarge it)
To get above features, you have to download the latest NetBeans daily build, then install Scala's plugins and Maven plugins.
For Scala plugins, see http://wiki.netbeans.org/Scala; For Maven plugins, see http://wiki.netbeans.org/MavenBestPractices.
Implementation of Scala for NetBeans based on GSF
The Scala for NetBeans is under pre-beta stage, other than bug-fixes, I'm preparing some documentations for it too. If you are interested in how to write language supporting under GSF's framework, you can take a look at this working documentation.
Implementation of Scala for NetBeans
And also:
Proposal of Scala for NetBeans
Progressing of Scala for NetBeans
Scala for NetBeans Screenshot#12: JUnit integration
There is a Scala JUnit Test template in Scala for NetBeans now, you can create Scala JUnit testing and run testing, see testing result.
Here is an example test file:
/* * DogTest.scala * * To change this template, choose Tools | Template Manager * and open the template in the editor. */ package scalajunit import org.junit.After import org.junit.Before import org.junit.Test import org.junit.Assert._ class DogTest { @Before def setUp() = { } @After def tearDown() = { } @Test def testHello() = { val dog = new Dog() dog.talk("Mr. Foo") assert(true) } @Test def testAdding() = { assertEquals(4, 2 + 3) } }
To get JUnit working, upgrade to Scala Project Module version 1.2.14, and re-create a new project for exist old project (if there has been one). Under the "Test Packages", create your testing packages, right click on package node, select create "Scala JUnit Test".
To run tests, right-click on project node, choose "Test".
And also, the beta-release is feature frozen, I'll concentrate on bug-fixes in next 1-2 weeks, and get it ready for public release as beta.
Employ Scala's Native Compiler in Scala for NetBeans
>>> Updated 3 hours later Fixed document displaying and go to declaration for Java element. Please update to Scala Editing version 1.1.4 ======
Well, it's a fairly long time after I latest blog about Scala for NetBeans. I was busy on several things, and can only work on this project on my spare time.
The good news is that I've integrated Scala's native compiler into this handy plugin, it means that the error messages shown in the editor will be the same as building now. And, the auto-completion feature is totally rewritten too, which also use AST tree that was created by Scala's native compiler to get all the candidate content assistant items.
The only problem is that the Java's document comments and offset to be go to when press CTRL + Click does not work properly yet, I have several way to resolve it, but I'm looking for a best way.
Another exciting news is that I've been invited to join NetBeans Dream Team, and of course, I accepted this invitation.
I'm planning to get Scala plugin for NetBeans to beta release in August, which will be compatible with NetBeans 6.5
Parsing Performance of Scala for NetBeans
I'm re-considering the indexing mechanism of Scala for NetBeans. The indexing is used to store meta-info of parsed templates/methods etc, for auto-completion and document/source offsets searching. Currently, the parsing phases include lexer, syntax and semantic analysis, not include type inference and type check.
With a basic performance testing on all Scala standard library and liftweb's library, the maxima parsing time seems less than 1s, the average parsing time is around 0.2s, not bad.
So, this may let the indexing feature a lot simple, I can store classes/objects/traits' meta-info only, instead of including their type parameters and their members (fields/methods, scoped importing etc), these additional information can be got via re-parsing the source file or querying the class file.
Bundled Latest Scala Runtime to Scala for NetBeans
First, you do not need to set SCALA_HOME any more to get whole plugins working. But, you can still set SCALA_HOME to specifying the target Scala version, if so, you should also need to download and unzip source jars to $SCALA_HOME/src;
Second, I'll begin to write some code in Scala instead of Java for Scala plugins. I can evaluate the features of Scala plugins in daily work, find and fix more bugs of plugins.
Scala for NetBeans Screenshot#12: Better Completion with More Types Inferred
Mostly, infix expressions can be type inferred now. CTRL+Click on infix op name, it will bring you to declaration (since 1.0.29.0)
===
Well, the type inference work is not so easy (with performance in mind), but anyway, I've got a bit more progress, at least, the chained member call can now be correctly inferred in a lot of cases. It's some level as Tor's JavaScript for NetBeans now.
First, let's create a val "node", which is a "scala.xml.Node"
Then, input '.' to invoke completion, as I know which type is of "node", the proposal items look good.
I choose "descendant" function (which returns a "List"), and input '.' again, we can see the proposal items look still good.
These features also work on Java's class.
Known issues:
- It seems the indexing/scanning for Scala standard library source will perform twice when you first installed Scala plugins
- The type inference is not consistence yet, so don't be strange for the strange behavior sometimes
Again, don't forget to download scala standard library's source jars and unzip to $SCALA_HOME/src, per sub-folder per jar
Which Programming Language J. Gosling Would Use Now, Except Java?
Maybe we can get completeness of J. Gosling's opinions about Java/Scaka/JVM from here
===
According to Adam Bien's blog from JavaOne
During a meeting in the Community Corner (java.net booth) with James Gosling, a participant asked an interesting question: "Which Programming Language would you use *now* on top of JVM, except Java?". The answer was surprisingly fast and very clear: - Scala.
I think Fortress will also be a very good future choice when it gets mature.
Scala for NetBeans Screenshot#11: Go to Remote Declaration and Doc Tooltip
These features work for Java classes too with a bit poor performance, I'll fix it later (fixed).
Not all identifiers have been type inferred, so these features are not applicable for all identifiers.
Please update to Scala Editing module as version 1.0.26.xxx when it's available, which is the only stable one these days. Remember to unzip Scala lib's source under $SCALA_HOME/src
Scala for NetBeans Screenshot#10: Working on Auto-Completion for Java
Click on the picture to enlarge it
Scala for NetBeans Screenshot#9: Working on Auto-Completion
To get this working, you should follow these steps:
- Update to newest Scala plugins (Editing version 1.0.21.1)
- Delete the old-cache files which are located at your NetBeans's configuration directory (for example, .netbeans/dev/var/cache).
- Download Scala standard library's source file, unzip them to $SCALA_HOME/src, per sub-folder per source jar file
Click on the picture to enlarge it
New Scala Plugins for NetBeans are Available for Public Test, and Fortress, Erlang
Due to the incompatibly changes of NetBeans underlaying modules, Scala plugins can not be installed/updated on NetBeans 6.1 any more, you should get the latest nightly build to play with Scala plugins.
===
>>> Updated Apr 19
- Fixed some broken syntax
- Fixed NPE caused by brace completion
- Fixed indentation of case class/object
- Added formatting options ("Preference" -> "Scala" -> "Formatting")
===
The new written Scala plugins for NetBeans are available for public test now, which can be installed on NetBeans 6.1 RC, and latest NetBeans nightly build. To get start, please visit http://wiki.netbeans.org/Scala
The following features are ready for test:
- Syntax highlighting
- Auto-indentation
- Brace completion
- Formatter
- Outline navigator
- Occurrences mark for local variables and functions
- Instance rename for local variables and functions
- Go-to-declaration for local variables and functions
- Scala project
- Basic debugger
And with known issues:
- Auto-completion it not fully supported yet and not smart
- There is no parsing errors recovering yet
- Semantic errors are not checked on editing, but will be noticed when you build project
- Due to the un-consistent of Scala's grammar reference document, there may be some syntax broken issues
BTW, Fortress editing plugin is also available on "Last Development Build" update center, see the installation part of http://wiki.netbeans.org/Scala to get it installed. It's a very alpha stage plugin.
And, Erlang plugins are also available from "Last Development Build" update center too, that is, you can install and use Erlang plugins with Ruby, Scala, JavaScript on the same NetBeans IDE (6.1 RC or nightly build). Thanks to Tor's work, the indexing performance has been improved a lot.
Erlang plugin will be rewritten in the near future too.
Scala for NetBeans Screenshot#8: Working on Indexer
I've done some basic indexer code, that is, all source files under a project will be parsed, analyzed, then indexed (class/object/trait, functions, fields etc). But it's just a start, before I finished type inference, if you press CTRL+SPACE to invoke completion, there are a lot of indexed Class/Object/Trait/Function will be roughly shown on you :-), it's not smart, it's more like a puzzle, you should decide which one is applicable by yourself. But you can get a view of the coming completion feature.
Click on the picture to enlarge it
Progress of Scala for NetBeans - with a New Written Lexer and Parser
According to this post:
I talked to Jetbrains about this, and they told me that they stopped working on the Scala plugin for the time being, because - demand for Groovy/Ruby was higher - the language was moving too fast - Scala is a terribly difficult language for compiler/tool writers, and the only good way to analyze Scala programs might be through the official compiler, which didn't yet support this
It's true that "Scala is a terribly difficult language for compiler/tool writers", but I'm trying to bypass "the only good way to analyze Scala programs might be through the official compiler"
Before rewriting Scala for NetBeans, I considered some parser choices, one was Scala's native compiler, which is good for compiling/building Scala project, but not suitable for Editor. And JavaCC, ANTLR, which may be good enough, but it's not natural to express Scala's grammar.
Then I found Rats! which is used by Fortress, a very very clean, powerful parser generator. After couple of days working, I got an incremental lexer for Scala, and a parser for Scala that with Scala's grammar being naturally expressed (the grammar definition is ParserScala.rats). The benefit of a complete controllable parser is that I can now do some type inference and wholly semantic analysis freely and immediately.
Another progress is that I've decoupled the Scala project's dependency on Java.source's classpath in NetBeans, instead, GSF's classpath is used in Scala project module now. That means, I can begin the indexer for Scala's standard library and project source files.
The next steps will be type inference; smart completion with type inferred information; indexer for later refectory and usages searching; parsing error recover etc.
Where We Are - Rewriting Scala for NetBeans
>>> Updated:
The rewritten plugin will be available, depending on the NetBeans' nightly build, it may be available after 10 hours or more. New features include a formatter (CTRL+SHIFT+F), and better brace completer. Smart auto-completion does not work now, needs further working.
===
The new Scala for NetBeans is going to form a good shape, I've got syntax highlighting, indentation, formatting, brace matching, and basic structure outline working. Here's a snapshot showing complex Scala statements being highlighted and reformatted properly:
Begin Rewriting Scala for NetBeans
I'm re-writing Scala for NetBeans. Everything is broken except syntax highlighting.
Please be patient to wait for further progress.
Developing IDE Based on GSF for NetBeans#1 - Minimal Support
There has been GSF (Generic Scripting Framework) which is Tor's working derived and abstracted from Java supporting code, and, the base of Ruby/JavaScript support for NetBeans.
So, how to develop an IDE based on GSF for NetBeans? I'd like to share some experiences in this series of articles, a series of outline description, without too much code and details, for detailed information, please go into the source code on hg.netbeans.org
I. Minimal Support - Highlighting
To implement a minimal support of your editor, you need to implement/extend following classes/interface:
public class ScalaLanguage implements GsfLanguage public class ScalaMimeResolver extends MIMEResolver public enum ScalaTokenId implements TokenId public class ScalaLexer implements Lexer<ScalaTokenId>
Where ScalaLexer is the token scanner of your language.
Then, register your language in layer.xml:
<filesystem> <folder name="Editors"> <folder name="text"> <folder name="x-scala"> <attr name="SystemFileSystem.localizingBundle" stringvalue="org.netbeans.modules.scala.editing.Bundle"/> <file name="language.instance"> <attr name="instanceCreate" methodvalue="org.netbeans.modules.scala.editing.lexer.ScalaTokenId.language"/> <attr name="instanceOf" stringvalue="org.netbeans.api.lexer.Language"/> </file> <folder name="FontsColors"> <folder name="NetBeans"> <folder name="Defaults"> <file name="coloring.xml" url="fontsColors.xml"> <attr name="SystemFileSystem.localizingBundle" stringvalue="org.netbeans.modules.scala.editing.Bundle"/> </file> </folder> </folder> </folder> <folder name="CodeTemplates"> <folder name="Defaults"> <file name="codetemplates.xml" url="codetemplates.xml"> <attr name="SystemFileSystem.localizingBundle" stringvalue="org.netbeans.modules.scala.editing.Bundle"/> </file> </folder> </folder> <folder name="Keybindings"> <folder name="NetBeans"> <folder name="Defaults"> <file name="org-netbeans-modules-scala-editing-keybindings.xml" url="DefaultKeyBindings.xml"/> </folder> </folder> </folder> </folder> </folder> </folder> <folder name="GsfPlugins"> <folder name="text"> <folder name="x-scala"> <file name="language.instance"> <attr name="instanceOf" stringvalue="org.netbeans.modules.gsf.api.GsfLanguage"/> <attr name="instanceClass" stringvalue="org.netbeans.modules.scala.editing.ScalaLanguage"/> </file> </folder> </folder> </folder> <folder name="Loaders"> <folder name="text"> <folder name="x-scala"> <attr name="SystemFileSystem.icon" urlvalue="nbresloc:/org/netbeans/modules/scala/editing/resources/scala16x16.png"/> <attr name="iconBase" stringvalue="org/netbeans/modules/scala/editing/resources/scala16x16.png"/> <folder name="Actions"> <file name="OpenAction.instance"> <attr name="instanceClass" stringvalue="org.openide.actions.OpenAction"/> <attr name="position" intvalue="100"/> </file> <file name="Separator1.instance"> <attr name="instanceClass" stringvalue="javax.swing.JSeparator"/> <attr name="position" intvalue="200"/> </file> <file name="CutAction.instance"> <attr name="instanceClass" stringvalue="org.openide.actions.CutAction"/> <attr name="position" intvalue="300"/> </file> <file name="CopyAction.instance"> <attr name="instanceClass" stringvalue="org.openide.actions.CopyAction"/> <attr name="position" intvalue="400"/> </file> <file name="PasteAction.instance"> <attr name="instanceClass" stringvalue="org.openide.actions.PasteAction"/> <attr name="position" intvalue="500"/> </file> <file name="Separator2.instance"> <attr name="instanceClass" stringvalue="javax.swing.JSeparator"/> <attr name="position" intvalue="600"/> </file> <file name="NewAction.instance"> <attr name="instanceClass" stringvalue="org.openide.actions.NewAction"/> <attr name="position" intvalue="700"/> </file> <file name="DeleteAction.instance"> <attr name="instanceClass" stringvalue="org.openide.actions.DeleteAction"/> <attr name="position" intvalue="800"/> </file> <file name="RenameAction.instance"> <attr name="instanceClass" stringvalue="org.openide.actions.RenameAction"/> <attr name="position" intvalue="900"/> </file> <file name="Separator3.instance"> <attr name="instanceClass" stringvalue="javax.swing.JSeparator"/> <attr name="position" intvalue="1000"/> </file> <file name="SaveAsTemplateAction.instance"> <attr name="instanceClass" stringvalue="org.openide.actions.SaveAsTemplateAction"/> <attr name="position" intvalue="1100"/> </file> <file name="Separator4.instance"> <attr name="instanceClass" stringvalue="javax.swing.JSeparator"/> <attr name="position" intvalue="1200"/> </file> <file name="FileSystemAction.instance"> <attr name="instanceClass" stringvalue="org.openide.actions.FileSystemAction"/> <attr name="position" intvalue="1300"/> </file> <file name="Separator5.instance"> <attr name="instanceClass" stringvalue="javax.swing.JSeparator"/> <attr name="position" intvalue="1400"/> </file> <file name="ToolsAction.instance"> <attr name="instanceClass" stringvalue="org.openide.actions.ToolsAction"/> <attr name="position" intvalue="1500"/> </file> <file name="PropertiesAction.instance"> <attr name="instanceClass" stringvalue="org.openide.actions.PropertiesAction"/> <attr name="position" intvalue="1600"/> </file> </folder> </folder> </folder> </folder> </filesystem>
Don't forget to prepare all these resource files that registered in above layer.xml, such as scala16x16.png etc
After that, write an one-line service descriptor org.openide.filesystems.MIMEResolver under META-INF/services, which looks like
org.netbeans.modules.scala.editing.ScalaMimeResolver
That's it.
Where We Are - Stock Marketing of China
I bought a stock in Dec 7, 2007, which was delisted soon, till now, that's why I don't care recent down of China stock markets.
But it seems the stock I bought is near to be listed again, so, I did some quick computing, and here's a summary prediction of SSE Composite Index (000001.ss):
The index will bounce to about 5600 in April, then down to about 4000 at June/July.
But I'll be very care to rely on this prediction, because it's a quick result (a carefully result needs a lot of computing time) and looks like really no-directions in the near future.
Fortress for NetBeans Screenshot#1: Syntax Highlighting
The outline navigator will do some unicode rendering; Brace matching works.
===
I'm planning to re-write Scala editor module, based on a new parser (maybe Tor from Sun and Carl from Google will help this project too). But before that, I'd like to prove some thoughts. That comes Fortress for NetBeans.
To know more about Fortress, please go to http://projectfortress.sun.com/Projects/Community/
Fortress's grammar is expressed in PEG (Parsing Expression Grammars), and is implemented on Rats!, a clean PEG parser generator.
I got some interesting experiences on how to let Rats! become a simple lexical token stream generator, of course, it has some limits, but these lexical tokens are used only for basic/quick highlighting, brace-matching etc, I do not use it for syntax parsing and semantic analysis, so the limits are not an effect. The syntax parsing, semantic analysis and grammar checking will be from original Fortress parser.
Here's a screenshot of my experimental work:
Actually I've done a bit of semantic works too, thus you can see the function definitions were bold highlighted and the outline navigator window was almost there.
ErlyBird 0.16.0 Released - An Erlang IDE based on NetBeans
I'm pleased to announce ErlyBird 0.16.0, an Erlang IDE based on NetBeans. This is an important feature release in size of 25M. If you have latest NetBeans nightly build installed, you can also install ErlyBird modules via update center.
CHANGELOG:
- Project metadata file is changed, please see Notes
- Instant rename (put caret on variable or function name, press CTRL+R, when rename finished, press ENTER)
- Go-To-Declaration to macros that are defined included header files
- Fixed: Go-To-Declaration to -inlcudelib won't work again after this include header file was opened in editor once
- Fixed: syntax broken for packaged import attribute
- Fixed: syntax broken for wild attribute
- Completion suggestion will not search other projects
- Track GSF changes, reindex performance was improved a lot; Can live with other GSF based language support now (Ruby, Groovy etc)
Java JRE 5.0+ is required.
To download, please go to: http://sourceforge.net/project/showfiles.php?group_id=192439
To install:
- Unzip erlybird-bin-0.16.0-ide.zip to somewhere.
- Make sure 'erl.exe' or 'erl' is under your environment path
- For Windows user, execute 'bin/erlybird.exe'. For *nix user, 'bin/erlybird'.
- Check/set your OTP path. From [Tools]->[Options], click on 'Erlang', then 'Erlang Installation' tab, fill in the full path of your 'erl.exe' or 'erl' file. For instance: "C:/erl/bin/erl.exe"
- The default -Xmx option for jvm is set to 256M, ErlyBird now works good with less memory, such as -Xmx128M. If you want to increase/decrease it, please open the config file that is located at etc/erlybird.conf, set -J-Xmx of 'default_options'.
When run ErlyBird first time, the OTP libs will be indexed. The indexing time varies from 10 to 30 minutes deponding on your computer.
Notes:
- Since project metadata format is changed, to open old ErlyBird created project, you should modify project.xml which is located at your project folder: nbproject/project.xml, change line:
<type>org.netbeans.modules.languages.erlang.project</type>
to:
<type>org.netbeans.modules.erlang.project</type>
- If you have previous version ErlyBird installed, you should delete the old cache files which are located at:
- *nix: "${HOME}/.erlybird/dev"
- mac os x: "${HOME}/Library/Application Support/erlybird/dev"
- windows: "C:\Documents and Settings\yourusername\.erlybird\dev" or some where
The status of ErlyBird is still Alpha, feedbacks and bug reports are welcome.
Erlang for NetBeans (ErlyBird) Recent Updates
Erlang for NetBeans (ErlyBird) has been put on the NetBeans' mercurial trunk. I added a changelog page on http://wiki.netbeans.org/ErlangChangelog, where you can learn the latest progressing.
Here is a summary of recent updates:
20080223
- Instant rename, or refactoring for local vars and functions (put caret on var or function name, press CTRL+R)
- Fixed: syntax broken for packaged import attribute
- Fixed: syntax broken for wild attribute
- Completion suggestions will search on project's own paths only
- Track GSF changes: reindex performance was improved a lot; Can live with other GSF based language support now (Ruby, Groovy etc)
ErlyBird 0.16.0 will be available soon.
Scala for NetBeans Recent Updates
I added a changelog page on http://wiki.netbeans.org/ScalaChangeLog, where you can learn the latest progressing.
Here is a summary of recent updates:
20080214 (Needs latest nightly build)
- [Editing] fixed: highlighting of occurences is not properly refreshed
20080213 (Needs latest nightly build)
- [Editing] fixed some issues of indent and brace-completion
20080212 (Needs latest nightly build)
- [Editing] fixed: case pattern cause a parser error at the minus symbol before number. (Thanks to misterm for reporting)
- [Project] sync with java.commonapi. Needs latest nightly build
20080205
- [Editing] fixed some broken grammars. (Thanks to Michael Nischt for collecting)
- [Project] fixed environment property, now scala.home property may not need to be set. (Thanks to Denis)
20080204
- [Debugger] fixed exceptions when put cursor on code
- [Debugger] "Add watches" should work now
Working with NetBeans Mercurial Repository
Just back from snowboarding. I tried to hg pull -uv to update to the latest code base of NetBeans, but got:
** unknown exception encountered, details follow ** report bug details to http://www.selenic.com/mercurial/bts ** or mercurial@selenic.com ** Mercurial Distributed SCM (version 0.9.4) Traceback (most recent call last): File "/opt/local/bin/hg", line 11, in ? mercurial.commands.run() File "/opt/local/lib/python2.4/site-packages/mercurial/commands.py", line 3110, in run sys.exit(dispatch(sys.argv[1:], argv0=sys.argv[0])) File "/opt/local/lib/python2.4/site-packages/mercurial/commands.py", line 3107, in dispatch return cmdutil.runcatch(u, args, argv0=argv0) File "/opt/local/lib/python2.4/site-packages/mercurial/cmdutil.py", line 37, in runcatch return dispatch(ui, args, argv0=argv0) File "/opt/local/lib/python2.4/site-packages/mercurial/cmdutil.py", line 364, in dispatch ret = runcommand(ui, options, cmd, d) File "/opt/local/lib/python2.4/site-packages/mercurial/cmdutil.py", line 417, in runcommand return checkargs() File "/opt/local/lib/python2.4/site-packages/mercurial/cmdutil.py", line 373, in checkargs return cmdfunc() File "/opt/local/lib/python2.4/site-packages/mercurial/cmdutil.py", line 356, ind = lambda: func(ui, repo, *args, **cmdoptions) File "/opt/local/lib/python2.4/site-packages/mercurial/commands.py", line 2063, in pull return postincoming(ui, repo, modheads, opts['update']) File "/opt/local/lib/python2.4/site-packages/mercurial/commands.py", line 2001, in postincoming return hg.update(repo, repo.changelog.tip()) # update File "/opt/local/lib/python2.4/site-packages/mercurial/hg.py", line 248, in update stats = _merge.update(repo, node, False, False, None, None) File "/opt/local/lib/python2.4/site-packages/mercurial/merge.py", line 541, in update checkunknown(wc, p2) File "/opt/local/lib/python2.4/site-packages/mercurial/merge.py", line 65, in checkunknown for f in wctx.unknown(): File "/opt/local/lib/python2.4/site-packages/mercurial/context.py", line 413, in unknown def unknown(self): return self._status[4] File "/opt/local/lib/python2.4/site-packages/mercurial/context.py", line 369, in __getattr__ self._status = self._repo.status() File "/opt/local/lib/python2.4/site-packages/mercurial/localrepo.py", line 864, in status list_ignored, list_clean) File "/opt/local/lib/python2.4/site-packages/mercurial/dirstate.py", line 445, in status for src, fn, st in self.statwalk(files, match, ignored=list_ignored): File "/opt/local/lib/python2.4/site-packages/mercurial/dirstate.py", line 421, in statwalk sorted_ = [ x for x in findfiles(f) ] File "/opt/local/lib/python2.4/site-packages/mercurial/dirstate.py", line 382, in findfiles if not ignore(np): RuntimeError: internal error in regular expression engine
It was the second time I encountered this issue, I don't know why, so, I just upgraded my Mercurial from 0.9.4 to 0.9.5, and tried to get a new clone. I typed:
hg clone http://hg.netbeans.org
Everything seemed going smoothly, I had a new repository now. I copied my old hgrc to replace the un-configured one, and typed ant build, guess what? I got a lot of package javax.help does not exist, build failed. I knew that was because of that the binary files were not downloaded properly. It seemed I should leave the original hdrc there, and let ant hook the external.py. So I removed all extra lines except the original content of hgrc:
[paths] default = http://hg.netbeans.org/main/
And tried ant build again, this time it went smoothly again.
LESSON: Do not modify hgrc before you do first "ant build", which will hook external.py and add "decode", "encode" automatically.
Vacation for Snowboarding
Scala for NetBeans: Debugger Modules are Available for Test
=== Updated Feb 5 ==>
I just fixed a defect of debugger modules, the updated modules will be available after new nightly build successfully. Since the underlaying changes of NetBeans APIs, you may need to download a new nightly build.
===
Scala Debugger modules are available for preview and test, which can be installed via Update Center for newest nightly build version of NetBeans (I tested on NetBeans IDE 6.1 200802040003). Debug feature includes two modules: Scala Debugger and Scala Debugger Projects Integration (you can click on "Name" in "Available Plugins" tab to find them), for more getting started information, please see http://wiki.netbeans.org/Scala
Google Group:
- Ian create a google group for Scala for NetBeans: http://groups.google.com/group/scala-netbeans
Known Issues:
- "Run to cursor" does not work yet
- "Step into Scala standard library's code" is not supported yet
"Add watches" is not supported yet- Complex condictions are not tested yet
Adventure with Nightly Build:
- Nightly build version of NetBeans changes frequently, please check updates frequently too: "Tool"->"Plugins"->"Reload Category" (please update all available modules, including those not relating to Scala). If things are broken, re-download a new nightly build and try ...
Scala on NetBeans Modules on Update Center Now
>>> Updated Feb 2, 2008:
There is an issue in auto-generated Scala project's ant build-impl.xml file, which get the scala.home property wrongly set. I just fixed it, please wait the new Scala Project module to be available on Update Center, which version should be 1.2.1.
======
I've added Scala modules to nightly build cluster, so, you can get/update these modules via NetBeans plugins update center from today.
Since these modules are experimental, I recommend you to download a nightly build version NetBeans before get these modules, you can get the latest nightly build NetBeans from: http://bits.netbeans.org/download/trunk/nightly/latest/
To get the Scala modules, open menu: [Tools]->[Plugins]</b>, check [Setting] to ensure "Last Development Build" is in the list of Update Centers, which with Url:
Then in the [Available Plugins] tab, you can find the "Scala" category, which currently contains 3 modules: Scala Console, Scala Editing, Scala Project (you may need to click [Reload Category]). Select them and click [Install]
Notes:
- Don't forget to set your <b>SCALA_HOME</b> environment first, and append -J-Dscala.home=scalahomepath (for Windows users, try to append -J-Dscala.home=%SCALA_HOME%) to the end of "netbeans_default_options" in your netbeans.conf file, where, scalahomepath is your Scala home's absolute path. For example: /Users/dcaoyuan/apps/scala/share/scala/ (which contains sub-directory: bin, lib etc). netbeans.conf is located at "pathToNetBeansInstallationDirectory/etc", in Mac OSX, it could be:
- /Applications/NetBeans/NetBeans\ 6.0.app/Contents/Resources/NetBeans/etc, or ~/SomePath/netbeans/etc
- The Scala Project supporting has been rewritten, so if you have Scala projects created by previous Scala plugin, you should recreate new Scala project, and copy source files to this new created project's src directory.
- Debugger module is not released yet.
Scala for NetBeans Screenshot#7: Working on Debug II
So, I've found the cause of that can't add breakpoints on context of object. By setting the enclosing class name of object as object's name + "$", I can now add breakpoints upon object.
To reach here, I have to write an EditorContextImpl? for Scala, which will get all compilation information from Scala semantic analyzer. But, to get all debugging features working, I have still to process a bit more conditions. After that, I'll release these modules for public trying.
Click on the picture to enlarge it
Scala for NetBeans Screenshot#6: Working on Debug
=== Updated Jan 30, 08: ==>
The cause of adding breakpoints to object context is relative to complilationInfo's CompilationUnit in java.source module, that the ComplilationInfo is set by JavaSource#moveToPhase after call Iterable extends CompilationUnitTree> trees = currentInfo.getJavacTask().parse(new JavaFileObject[] {currentInfo.jfo}), thus the returning CompilationUnitTree is generated by sun's java parser tool, it of course can not be parsed properly with "object" keyword.
To resolve it, I have to rewrite a scala.debug.projects module, which will be derived from debugger.jpda.projects module
===
=== Updated Jan 26, 08: ==>
Yes, I've got a better Scala project support, not only debug works on pure Scala or Scala+Java project, but also got an UI for defining Main class of the application. Actually, Scala project support is now almost same as J2SE project on NetBeans. The only problem is, if you add a breakpoint on statements of a Scala object, since the org.netbeans.api.java.source.TreeUtilities#scopeFor will return a scope of "COMPILATION_UNIT", which will return null when is called by scope.getEnclosingClass() in org.netbeans.modules.debugger.jpda.projects.EditorContextImpl#getClassName, so the breakpoint will be invalid. To get this fixed, EditorContextImpl needs to be patched.
According to Scala FAQ, Scala object gets compiled to 2 classes, for example:
object HelloObj { val f = "one field" val x = 3 }
We will get:
public final class HelloObj extends java.lang.Object{ public static final int $tag(); public static final int x(); public static final java.lang.String f(); } public final class HelloObj$ extends java.lang.Object implements scala.ScalaObject{ public static final HelloObj$ MODULE$; public static {}; public HelloObj$(); public int x(); public java.lang.String f(); public int $tag(); }
Writing “HelloObj.x()” in Scala is the same as writing “HelloObj$.MODULE$.x()”, but the former should probably be preferred for readability.
The problem here is, when you add breakpoint at val x = 3 in the source file, what will be the breakpoint's scope in these two compiled classes (which one?), why scope.getClassName() will return null? anyone know what's the magic here?
===
I'm working on debug feature for Scala on NetBeans. I copied a smallest set of classes from Java Debugger module, and hacked something to make debug working on a mixed project (a Java project with Scala source files). By default, NetBeans IDE will step through Scala source files that are called (see Greetjan's article: Stepping through Groovy in NetBeans IDE ), but you can't add breakpoints directly on Scala source file. So the first thing that I should work on is let the user can add/remove breakpoints in Scala Editor, and get NetBeans' debugger stopping at these breakpoints. After a full day hacking, I got this working.
To got mouse click on adding/removing breakpoints working, you can implement and register a MIMEType related "GlyphGutterActions" in layer.xml, so, org.netbeans.modules.editor.impl.GlyphGutterActionsProvider#getGlyphGutterActions(String mimeType) can lookup this action. GlyphGutterActionsProvider#getGlyphGutterActions will be invoked by org.netbeans.editor.GlyphGutter#GutterMouseListener#mouseClicked, that's it.
The screenshot shows a mixed Java/Scala project, the example code was copied from Fred Janon's blog, it's a pretty simple example. I added a breakpoint on Dog.scala's val sound="Woof woof!" and another breakpoint at function talk()'s println(hi + sound), then invoke debug, the execution stopped at these two breakpoints as I expected, and you can see the context, for example, the values of variable "sound" and "hi" on debugging windows (at the bottom)
Of course, you can step into, step to cursor line too.
I need to get the debug feature working on a pure Scala project, I have not tried yet, but it seems I need to get the Scala project module better before that.
Click on the picture to enlarge it
On Travel
I'm in Vancouver Airport right now, using the new free Wi-Fi service here. After four-day trip to San Francisco, I'll fly to China for the traditional Spring Festival.
I met friends in San Francisco, we are developing something using Erlang as I mentioned before. What we' are building is somehow a "switch" for content, we've successfully got a lot of different content sources to be "switched" to standard Atom/Rss etc, with a new designed template language (based in Erlang). Actually It's a pleasure to write these switching code in Erlang (Parse, Map, Mashup) with the pattern match syntax and xmerl lib.
Scala Supporting for NetBeans Updated#2 - 20080112
Warning:
Scala for NetBeans is still under development. All these releases are experimental, they may be unstable yet.
Change Log:
- Can be installed on NetBeans 6.0
- Fixed some syntax broken
- Various bugs fixes
- Syntax checking, highlighting, code folding, navigator, basic indent
- Basic completion, In-place refactoring, occurrences marks
- Interactive Scala shell. [Windows] -> [Interactive Scala Shell]
- Basic Scala project management with file locator for compile Errors
- Requires NetBeans 6.0 or newest NetBeans Nightly build (get from: http://bits.netbeans.org/download/trunk/nightly/latest/)
- Don't forget to set your SCALA_HOME environment first, or append "-J-Dscala.home=scalahomepath" to the end of "netbeans_default_options" in your netbeans.conf file, where, "scalahomepath" is your Scala home's absolute path. For example: /Users/dcaoyuan/apps/scala/share/scala/ (For Windows users, it's better to set both of them).
The netbeans.conf is located at "pathToNetBeansInstallationDirectory/etc", in Mac OSX, it could be:
/Applications/NetBeans/NetBeans\ 6.0.app/Contents/Resources/NetBeans/etc
Known Issues:
- Do not write old-style ForComprehension "for (val i <- ...)", instead, use "for(i <- ...)". Please see Scala Spec 2.6.0+
- Do not put infix/postfix operator at the beginning of new line even in a parenthesis expression
- When "<" is an operator, put a space after "<" to identify it from a xml element
Download:
- http://sourceforge.net/project/showfiles.php?group_id=192439&package_id=256544
Installation:
- Upzip to some where, there will be several *.nbm files
- Run NetBeans, install these *.nbm files via [Tools] -> [Plugins] -> "Downloaded"
Scala Supporting for NetBeans Updated#1 - 20080110
Features:
- Syntax checking, highlighting, code folding, navigator, basic indent
- Basic completion, In-place refactoring, occurrences marks
- Interactive Scala shell. [Windows] -> [Interactive Scala Shell]
- Basic Scala project management with file locator for compile Errors
- Requires newest NetBeans Nightly build (get from: http://bits.netbeans.org/download/trunk/nightly/latest/)
- Don't forget to set your SCALA_HOME environment first, or append "-J-Dscala.home=scalahomepath" to the end of "netbeans_default_options" in your netbeans.conf file, where, "scalahomepath" is your Scala home's absolute path. For example: /Users/dcaoyuan/apps/scala/share/scala/
The netbeans.conf is located at "pathToNetBeansInstallationDirectory/etc", in Mac OSX, it could be:
/Applications/NetBeans/NetBeans\ 6.0.app/Contents/Resources/NetBeans/etc
Known Issues:
- Do not write old-style ForComprehension "for (val i <- ...)", instead, use "for(i <- ...)". Please see Scala Spec 2.6.0+
- Do not put infix/postfix operator at the beginning of new line even in a parenthesis expression
- When "<" is an operator, put a space after "<" to identify it from a xml element
Download:
- http://sourceforge.net/project/showfiles.php?group_id=192439&package_id=256544
Installation:
- Upzip to some where, there will be several *.nbm files
- Run NetBeans, install these *.nbm files via [Tools] -> [Plugins] -> "Downloaded"
Scala for NetBeans Screenshot#5: In-Place Refactoring
I've got Scala's semantic analysis more tightly integrated into NetBeans' Schliemann module. The first benefit is In-Place Refactoring (Instant Rename).
On the following screenshots, first, you put caret on a variable name ("scope", for example), the occurrences of this variable will be hi-lighted. Then, press key Ctrl+R, the highlighting color will be changed with meaning of ready for rename.
Just type or modify the name to new one, then press Enter. The occurrences of "scope" will be changed to new name, for example: "scope1".
And, YES, the Erlang for NetBeans will also get this benefit.
The Year That Was, The Year That Will Be
AIOTrade
It reached 36,000 download. I use AIOTrade for my trading, via my Neural Network prediction. For 2007, I invested 1 and got almost 300%, not bad. For trading in 2008, I'm now on opened position, let me see what another year will be. For AIOTrade itself in 2008, you may have guessed (or not), I'm planning to rewrite AIOTrade in Scala.
ErlyBird
ErlyBird is now 0.15.2. It reached 2,500 downlaod. For 2008, I hope to integrate more features into NetBeans Schliemann project. For 2008, Erlang as a programming language will not be the choice for AIOTrade, I may tell why some day.
Scala for NetBeans
I'm a such lazy man, that I have to write some helpful tools before I do something. That's why, before trade, I had to write AIOTrade, before rewrite AIOTrade (Plan A - in Erlang, now dropped), I have to write ErlyBird, before rewrite AIOTrade (Plan B - in Scala) I have to write Scala for NetBeans. That's the cause-chain. Thanks God, I do not need to invent another language, there has been Scala.
Well, Scala looks very powerful and interesting, it's what in my eyes, a clean mix of Java + Ruby + Erlang, or, the JRE (Just Running Environment).
But it's so powerful in syntax, that is so difficult for a truly IDE supporting, that is so important to have an IDE as assistant (which will help writing Scala a lot). And, since I'm going to build things upon NetBeans as NetBeans' modules, the IDE of course has to be based upon NetBeans. I really hope there had been a good Scala supporting for NetBeans, but for 2007, there was none. So, how about 2008?
Happy New Year, everybody.
Happy New Year, especially to Beijing, the 2008 Olympic Game, although Life Is Always Elsewhere.
Scala for NetBeans Screenshot#4: Basic Completion
Scala for NetBeans now supports basic completion for keywords, functions of Predef, local var/val/function etc.
The screenshot shows the popup with candidate items when you typed "s" and pressed Ctrl+Space, including functions: scalProd(), sum() and val: scope in context of for.scala
Click on the picture to enlarge it
Scala for NetBeans Screenshot#3: Preliminary Marking Occurrences and Goto Declaration
When you put the caret on a var, val, or function id, for example, "userName" on the below screenshot, the occurrences of this var/val/function will be highlighting, and, on the right side bar, there are colored indicators telling you where are these occurrences.
You can also go to declaration of var/val/function too (only in same source file currently).
What does this mean? It means I've begun to do some semantic analysis on parsed AST.
Click on the picture to enlarge it
First Experimental Scala Supporting for NetBeans is Available
>>> Updated Dec 23: There is an updated Editing module at:
http://sourceforge.net/project/showfiles.php?group_id=192439&package_id=256544&release_id=563760
CHANGELOG:
- "{" of class/object/trait declarations can be put on new line
- Some broken syntax fixed.
========
Features:
- Syntax checking, highlighting, code folding, navigator, basic indent
- Interactive Scala shell. [Windows] -> [Interactive Scala Shell]
- Basic Scala project management with file locator for compile Errors
- Requires NetBeans 6.0 Release
- Don't forget to set your SCALA_HOME environment first, or append "-J-Dscala.home=scalahomepath" to the end of "netbeans_default_options" in your netbeans.conf file, where, "scalahomepath" is your Scala home's absolute path. For example: /Users/dcaoyuan/apps/scala/share/scala/
The netbeans.conf is located at "pathToNetBeansInstallationDirectory/etc", in Mac OSX, it could be:
/Applications/NetBeans/NetBeans 6.0.app/Contents/Resources/NetBeans/etc
Know Issues:
- Embedded /* */ comment is not supported yet.
- Do not write old-style ForComprehension "for (val i <- ...)", instead, use "for(i <- ...)". Please see Scala Spec 2.6.0+
- Do not put infix/postfix operator at the beginning of new line even in a parenthesis expression
- When "<" is an operator, put a space after "<" to identify it from a xml element
Download:
- http://sourceforge.net/project/showfiles.php?group_id=192439&package_id=256544
Installation:
- Upzip to some where, there will be several *.nbm files
- Run NetBeans, install these *.nbm files via [Tools] -> [Plugins] -> "Downloaded"
Scala for Netbeans Screenshot#2: Scala Console and File Locator for Compile Errors
The interactive Scala shell console works now, with 'up'/'down' key for command history.
And as ant based Scala project, the project building just works fine, in the building output window, you can click on the compiler error message to jump to the corresponding location of source file.
You may have also noticed, if an id is used as operator, it's highlighting in special color.
The case clauses are listed in the outline window.
Click on the picture to enlarge it
ErlyBird 0.15.2 Released - An Erlang IDE based on NetBeans
I'm pleased to announce ErlyBird 0.15.2, an Erlang IDE based on NetBeans. This is an important feature release in size of 17.9M.
CHANGELOG:
- Supported OTP/Erlang R12B new syntax.
- A new Emacs standard color theme.
- Fixed some formatter bugs.
- Better syntax error message.
- Various bugs fixes.
To switch color theme, open [Tools]->[Options], click on 'Fonts & Colors', choose 'Profile' drop-down box.
Java JRE 5.0+ is required.
To download, please go to: http://sourceforge.net/project/showfiles.php?group_id=192439
To install:
- Unzip erlybird-bin-0.15.2-ide.zip to somewhere.
- Make sure 'erl.exe' or 'erl' is under your environment path
- For Windows user, execute 'bin/erlybird.exe'. For *nix user, 'bin/erlybird'.
- Check/set your OTP path. From [Tools]->[Options], click on 'Erlang', then 'Erlang Installation' tab, fill in the full path of your 'erl.exe' or 'erl' file. For instance: "C:/erl/bin/erl.exe"
- The default -Xmx option for jvm is set to 256M, ErlyBird now works good with less memory, such as -Xmx128M. If you want to increase/decrease it, please open the config file that is located at etc/erlybird.conf, set -J-Xmx of 'default_options'.
When run ErlyBird first time, the OTP libs will be indexed. The indexing time varies from 10 to 30 minutes deponding on your computer.
Notice:
If you have previous version ErlyBird installed, it's recommended to delete the old cache files which are located at:
- *nix: "${HOME}/.erlybird/dev"
- mac os x: "${HOME}/Library/Application Support/erlybird/dev"
- windows: "C:\Documents and Settings\yourusername\.erlybird\dev" or some where
The status of ErlyBird is still Alpha, feedbacks and bug reports are welcome.
Scala Support for NetBeans Screenshot#1: Syntax Highlighting and Scala Project
Scala editor module has been integrated with existing Scala Project module. The xml syntax is almost supported. There are still a little bit complex syntax not be supported yet.
I hope a downloadable Scala modules package can be ready in one week, so you can get it from the NetBeans update center.
BTW, new version of ErlyBird is not ready yet, I'm waiting for fixing of some issues in NetBeans' Generic Languages Framework module.
Screen snapshot: (Click to enlarge)
Scala Editor for NetBeans
>>> Updated Dec 10: I've been granted developer role for netbeans' languages project, and committed the basic editing support code.
========
I wrote another NetBeans language supporting module, A Scala Editor. I've got most syntax working with syntax checking, highlighting, fold, navigator etc, except the xml syntax.
Since Scala is a newline aware language, the LL(k) grammar definitions is a challenge, I spent all my 2 days of this weekend to reach here.
There are a lot of works left for a full featured editor, after that, I'll release it to public, and contribute to NetBeans community.
Here's a screen snapshot: (Click to enlarge)
ErlyBird Is Ready for R12B
OTP/Erlang R12B is coming soon, which will support Binary Comprehension and -spec, -type attributes. I've updated ErlyBird to support these new syntax, and, with a new Emacs Standard color theme.
The new version of ErlyBird will be available around the releasing date of OTP/Erlang R12B and NetBeans 6.0
Regexp Syntax in Pattern Match?
Pattern match in Erlang is very useful, but it has some limits, for example, to match something like:
\d\d\dx/\d\d\d\d/\d\d/\d\d/
I have to write the code as:
<<_:S/binary, C1,C2,C3,$x,$/,Y1,Y2,Y3,Y4,$/,M1,M2,$/,D1,D2,$/,_/binary>> when C1 > 47, C1 < 58, C2 > 47, C2 < 58, C3 > 47, C3 < 58, Y1 > 47, Y1 < 58, Y2 > 47, Y2 < 58, Y3 > 47, Y3 < 58, Y4 > 47, Y4 < 58, M1 > 47, M1 < 58, M2 > 47, M2 < 58, D1 > 47, D2 < 58, D2 > 47, D2 < 58
But life can be simple, by using parse_transform, we can write above code to:
<<_:S/binary,d,d,d,$x,$/,d,d,d,d,$/,d,d,$/,d,d,$/,_/binary>>Where d is digital. The parse_trasnform can process the AST tree to make the true code.
And more, since current erl_parse supports any atom in binary skeleton, we can write pattern match as:
<<_:S/binary,'[a-z]','[^abc]','var[0-9]',$x,$/,d,d,d,d,$/,d,d,$/,d,d,$/,_/binary>>
Will somebody write such a parse_transform?
Tim Bray's Erlang Exercise "WideFinder" - After Find Wide
>>> Updated Nov 7:
A new version tbray9a.erl which uses ets instead of dict, with 116 LoC, took about 3.8 sec on 5 million lines log file, 0.93 sec on 1 million lines file on my 4-core linux box.
- Results on T5120, an 8-core 1.4 GHz machine with 2 integer instruction threads per core and support for 8 thread contexts per core. Solaris thinks it sees 64 CPUs:
Schedulers# | Elapsed(s) | User(s) | System(s) | (User+System)/Elapsed |
1 | 37.58 | 35.51 | 7.82 | 1.15 |
2 | 20.14 | 35.31 | 8.28 | 2.16 |
4 | 11.81 | 35.37 | 8.25 | 3.69 |
8 | 7.63 | 35.28 | 8.33 | 5.72 |
16 | 5.60 | 36.08 | 8.27 | 7.92 |
32 | 5.29 | 36.64 | 8.11 | 8.46 |
64 | 5.45 | 36.79 | 8.23 | 8.26 |
128 | 5.26 | 36.75 | 8.39 | 8.58 |
When schedulers was 16, (User+System)/Elapsed was 7.92, and the elapsed time reached 5.60 sec (near the best), so, the 8-core computing ability and 2 x 8 =16 integer instruction threads ability almost reached the maxima. The 8 x 8 thread contexts seemed to do less help on gaining more performance improvement.
It seems that T5120 is a box with 8-core parallel computing ability and 16-thread for scheduler?
On my 4-core linux box, the slowest time (1 scheduler) vs the fastest time (128 schedulers) was 6.627/3.763 = 1.76. On this T5120, was 37.58/5.26 = 7.14. So, with the 8-core and 16-integer-instruction-thread combination, the T5120 is pretty good on parallel computing.
An interesting point is, when schedulers increased, Elpased time dropped along with User time and System time keeping almost constancy. This may because I separated the parallelized / sequential part completely in my code.
- Results on 2.80Ghz 4-core Intel xeon linux box (5 million lines log file):
Schedulers# | Elapsed(s) | User(s) | System(s) | (User+System)/Elapsed |
1 | 6.627 | 5.356 | 4.248 | 1.45 |
2 | 4.486 | 6.176 | 3.936 | 2.25 |
4 | 4.299 | 8.989 | 4.156 | 3.06 |
8 | 3.960 | 9.629 | 3.644 | 3.35 |
16 | 3.826 | 9.101 | 3.696 | 3.34 |
32 | 3.858 | 9.029 | 3.840 | 3.34 |
64 | 3.763 | 8.801 | 3.820 | 3.35 |
128 | 3.920 | 9.137 | 3.980 | 3.35 |
========
I'm a widefinder these days, and after weeks found wide, I worte another concise and fast widefinder tbray9.erl, which is based on Steve, Anders and my previous code, with Boyer-Moore searching (It seems Python's findall uses this algorithm) and parallelized file reading. It's in 120 LoC (a bit shorter than Fredrik Lundh's wf-6.py), took about 1 sec for 1 million lines log file, and 5.2 sec for 5 million lines on my 4-core linux box. Got 5.29 sec on T5120 per Tim's testing.
To evaluate:
erlc -smp tbray9.erl erl -smp +A 1024 +h 10240 -noshell -run tbray9 start o1000k.ap
BTW since I use parallelized io heavily, by adding flag +A 1024, the code can get 4.3 sec for 5 million lines log file on my 4-core linux box.
Binary efficiency in Erlang is an interesting topic, except some tips I talked about in previouse blog, it seems also depending on binary size, memory size etc., The best buffer size for my code seems to be around 20000k to 80000k, which is the best range on my 4-core linux box and T5120, but it may vary for different code.
Note: There is a maximum element size limit of 227 - 1 (about 131072k) for binary pattern matching in current Erlang, this would be consistent with using a 32-bit word to store the size value (with 4 of those bits used for a type identifier and 1 bit for a sign indicator) (For this topic, please see Philip Robinson's blog). So, the buffer size can not be great than 131072k.
I. Boyer-Moore searching
Thanks to Steve and Anders, they've given out a concise Boyer-Moore searching algorithm in Erlang, I can modify it a bit to get a general BM searching module for ASCII encoded binary:
%% Boyer-Moore searching on ASCII encoded binary -module(bmsearch). -export([compile/1, match/3]). -record(bmCtx, {pat, len, tab}). compile(Str) -> Len = length(Str), Default = dict:from_list([{C, Len} || C <- lists:seq(1, 255)]), Dict = set_shifts(Str, Len, 1, Default), Tab = list_to_tuple([Pos || {_, Pos} <- lists:sort(dict:to_list(Dict))]), #bmCtx{pat = lists:reverse(Str), len = Len, tab = Tab}. set_shifts([], _, _, Dict) -> Dict; set_shifts([C|T], StrLen, Pos, Dict) -> set_shifts(T, StrLen, Pos + 1, dict:store(C, StrLen - Pos, Dict)). %% @spec match(Bin, Start, #bmCtx) -> {true, Len} | {false, SkipLen} match(Bin, S, #bmCtx{pat=Pat, len=Len, tab=Tab}) -> match_1(Bin, S + Len - 1, Pat, Len, Tab, 0). match_1(Bin, S, [C|T], Len, Tab, Count) -> <<_:S/binary, C1, _/binary>> = Bin, case C1 of C -> match_1(Bin, S - 1, T, Len, Tab, Count + 1); _ -> case element(C1, Tab) of Len -> {false, Len}; Shift when Shift =< Count -> {false, 1}; Shift -> {false, Shift - Count} end end; match_1(_, _, [], Len, _, _) -> {true, Len}.
Usage:
> Pattern = bmsearch:compile("is a"). {bmCtx,"a si",4,{4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,...}} > Bin = <<"this is a test">>. <<"this is a test">> > bmsearch:match(Bin, 1, Pattern). {false,1} > bmsearch:match(Bin, 5, Pattern). {true, 4} > bmsearch:match(Bin, 7, Pattern). {false, 4}
II. Reading file in parallel and scan
To read file in parallel, we should open a new file handle for each process. To resolve the line break bound, we just split each chunk to first line (Head), line-bounded Data, and last line (Tail), and mark Head, Tail with the serial number as {I * 10, Head}, {I * 10 + 1 Tail}, so we can join all pending segments (Head and Tail of each chunk) later in proper order.
scan_file({FileName, Size, I, BmCtx}) -> {ok, File} = file:open(FileName, [raw, binary]), {ok, Bin} = file:pread(File, Size * I, Size), file:close(File), HeadL = split_on_first_newline(Bin), TailS = split_on_last_newline(Bin), DataL = TailS - HeadL, <<Head:HeadL/binary, Data:DataL/binary, Tail/binary>> = Bin, {scan_chunk({Data, BmCtx}), {I * 10, Head}, {I * 10 + 1, Tail}}.
III. Spawn workers
["Luke's spawn_worker" Luke's spawn_worker] are two small functions, they are very useful, stable and good abstract on processing workers:
spawn_worker(Parent, F, A) -> erlang:spawn_monitor(fun() -> Parent ! {self(), F(A)} end). wait_result({Pid, Ref}) -> receive {'DOWN', Ref, _, _, normal} -> receive {Pid, Result} -> Result end; {'DOWN', Ref, _, _, Reason} -> exit(Reason) end.
So, I can start a series of workers simply by:
read_file(FileName, Size, ProcN, BmCtx) -> [spawn_worker(self(), fun scan_file/1, {FileName, Size, I, BmCtx}) || I <- lists:seq(0, ProcN - 1)].
And then collect the results by:
Results = [wait_result(Worker) || Worker <- read_file(FileName, ?BUFFER_SIZE, ProcN1, BmCtx)],
In this case, returning Results is a list of {Dict, {Seq1, Head}, {Seq2, Tail}}
IV. Concat segments to a binary and scan it
Each chunk is slipt to Head (first line), line-bounded Data, and Tail (last line). The Head and Tail segments are pending for further processing. After all workers finished scanning Data (got a Dict), we can finally sort these pending segments by SeqNum?, concat and scan them in main process.
Unzip the results to Dicts and Segs, sort Segs by SeqNum?:
{Dicts, Segs} = lists:foldl(fun ({Dict, Head, Tail}, {Dicts, Segs}) -> {[Dict | Dicts], [Head, Tail | Segs]} end, {[], []}, Results), Segs1 = [Seg || {_, Seg} <- lists:keysort(1, Segs)],
The sorted Segments is a list of binary, list_to_binary/1 can concat them to one binary efficently, you do not need to care about if it's a deep list, they will be flatten automatically:
Dict = scan_chunk({list_to_binary(Segs1), BmCtx}),
Conclution
Thinking in parallel is fairly simple in Erlang, right? Most of the code can run in one process, and if you want to spawn them for parallel, you do not need to modify the code too much. In this case, scan_file/4 is sequential, which just return all you want, you can spawn a lot of workers which do scan_file/4 work, then collect the results later. That's all.
Learning Coding Binary (Was Tim's Erlang Exercise - Round VI)
>>> Updated Nov 1:
Tim tested tbray5.erl on T5120, for his 971,538,252 bytes of data in 4,625,236 lines log file, got:
real 0m20.74s user 3m51.33s sys 0m8.00s
The result was what I guessed, since the elapsed time of my code was 3 times of Anders' on my machine. I'm glad that Erlang performs linearly on different machines/os.
My code not the fastest. I did not apply Boyer-Moore searching, thus scan_chunk_1/4 has to test/skip binary 1byte by 1byte when not exactly matched. Anyway, this code shows how to code binary efficiently, and demos the performance of traversing binary byte by byte (the performance is not so bad now, right?). And also, it's what I want: a balance between simple, readable and speed.
Another approach for lazy man is something binary pattern match hacking, we can modify scan_chunk_1/4 to:
scan_chunk_1(Bin, DataL, S, Dict) when S < DataL - 34 -> Offset = case Bin of <<_:S/binary,"GET /ongoing/When/",_,_,_,$x,$/,_,_,_,_,$/,_,_,$/,_,_,$/,_/binary>> -> 34; <<_:S/binary,_,"GET /ongoing/When/",_,_,_,$x,$/,_,_,_,_,$/,_,_,$/,_,_,$/,_/binary>> -> 35; <<_:S/binary,_,_,"GET /ongoing/When/",_,_,_,$x,$/,_,_,_,_,$/,_,_,$/,_,_,$/,_/binary>> -> 36; <<_:S/binary,_,_,_,"GET /ongoing/When/",_,_,_,$x,$/,_,_,_,_,$/,_,_,$/,_,_,$/,_/binary>> -> 37; <<_:S/binary,_,_,_,_,"GET /ongoing/When/",_,_,_,$x,$/,_,_,_,_,$/,_,_,$/,_,_,$/,_/binary>> -> 38; <<_:S/binary,_,_,_,_,_,"GET /ongoing/When/",_,_,_,$x,$/,_,_,_,_,$/,_,_,$/,_,_,$/,_/binary>> -> 39; <<_:S/binary,_,_,_,_,_,_,"GET /ongoing/When/",_,_,_,$x,$/,_,_,_,_,$/,_,_,$/,_,_,$/,_/binary>> -> 40; <<_:S/binary,_,_,_,_,_,_,_,"GET /ongoing/When/",_,_,_,$x,$/,_,_,_,_,$/,_,_,$/,_,_,$/,_/binary>> -> 41; <<_:S/binary,_,_,_,_,_,_,_,_,"GET /ongoing/When/",_,_,_,$x,$/,_,_,_,_,$/,_,_,$/,_,_,$/,_/binary>> -> 42; <<_:S/binary,_,_,_,_,_,_,_,_,_,"GET /ongoing/When/",_,_,_,$x,$/,_,_,_,_,$/,_,_,$/,_,_,$/,_/binary>> -> 43; _ -> undefined end, case Offset of undefined -> scan_chunk_1(Bin, DataL, S + 10, Dict); _ -> case match_until_space_newline(Bin, S + Offset) of {true, E} -> Skip = S + Offset - 12, L = E - Skip, <<_:Skip/binary,Key:L/binary,_/binary>> = Bin, scan_chunk_1(Bin, DataL, E + 1, dict:update_counter(Key, 1, Dict)); {false, E} -> scan_chunk_1(Bin, DataL, E + 1, Dict) end end; scan_chunk_1(_, _, _, Dict) -> Dict.
The elapsed time dropped to 1.424 sec immediatley vs 2.792 sec before, speedup about 100%, on my 4-CPU linux box.
If you are patient, you can copy-paste 100... such lines :-) (in this case, I'd rather to pick Boyer-Moore), and the elapsed time will drop a little bit more, but not much after 10 lines or so.
========
>>> Updated Oct 29:
Pihis updated his WideFinder, applied guildline II, and beat Ruby on his one-core box. And he also modified Anders's code by removing all un-necessary remaining binary bindings (which cause un-necessay sub-binary splitting), then, Steve tested the refined code, and got 0.567s on his famous 8-CPU linux box. Now, we may reach the real Disk/IO bound, should we try parallelized file reading? but, I've tired of more widefinders. BTW, May we have regexped pattern match in syntax level? The End.
========
>>> Updated Oct 26:
Code cleanup
========
Binary usaully is more efficent than List in Erlang.
The memory size of Binary is 3 to 6 words plus Data itself, the Data can be allocated / deallocated in global heap, so the Data can be shared over function calls, shared over processes when do message passing (on the same node), without copying. That's why heap size affects a lot on binary.
The memory size of List is 1 word per element + the size of each element, and List is always copying between function calls, on message passing.
In my previous blogs about Tim's exercise, I suspected the performance of Binary traverse. But, with more advices, experience, it seems binary can work very efficient as an ideal Dataset processing struct.
But, there are some guidelines for efficient binary in Erlang, I'll try to give out here, which I learned from the exercise and experts.
I. Don't split a binary unless the split binaries are what you exactly want
Splitting/combining binaries is expensive, so when you want to get values from a binary at some offsets:
Do
<<_:Offset/binary, C, _/binary>> = Bin, io:format("Char at Offset: ~p", [C]).
Do Not *
<<_:Offset/binary, C/binary, _/binary>> = Bin, io:format("Char at Offset: ~p", [C]).
* This may be good in R12B
And when you want to split a binary to get Head or Tail only:
Do
<<Head:Offset/binary,_/binary>> = Bin.
Do Not
{Head, _} = split_binary(Bin, Offset).
II. Calculate the final offsets first, then split it when you've got the exactly offsets
When you traverse binary to test the bytes/bits, calculate and collect the final offsets first, don't split binary (bind named Var to sub-binary) at that time. When you've got all the exactly offsets, split what you want finally:
Do
get_nth_word(Bin, N) -> Offsets = calc_word_offsets(Bin, 0, [0]), S = element(N, Offsets), E = element(N + 1, Offsets), L = E - S, <<_:S/binary,Word:L/binary,_/binary>> = Bin, io:format("nth Word: ~p", [Word]). calc_word_offsets(Bin, Offset, Acc) when Offset < size(Bin) -> case Bin of <<_:Offset/binary,$ ,_/binary>> -> calc_word_offsets(Bin, Offset + 1, [Offset + 1 | Acc]); _ -> calc_word_offsets(Bin, Offset + 1, Acc) end; calc_word_offsets(_, _, Acc) -> list_to_tuple(lists:reverse(Acc)). Bin = <<"This is a binary test">>, get_nth_word(Bin, 4). % <<"binary ">>
Do Not
get_nth_word_bad(Bin, N) -> Words = split_words(Bin, 0, []), Word = element(N, Words), io:format("nth Word: ~p", [Word]). split_words(Bin, Offset, Acc) -> case Bin of <<Word:Offset/binary,$ ,Rest/binary>> -> split_words(Rest, 0, [Word | Acc]); <<_:Offset/binary,_,_/binary>> -> split_words(Bin, Offset + 1, Acc); _ -> list_to_tuple(lists:reverse([Bin | Acc])) end. Bin = <<"This is a binary test">>, get_nth_word_bad(Bin, 4). % <<"binary">>
III. Use "+h Size" option or [{min_heap_size, Size}] with spawn_opt
This is very important for binary performance. It's somehow a Key Number for binary performance. With this option set properly, the binary performs very well, otherwise, worse.
IV. Others
- Don't forget to compile to native by adding "-compile([native])." in your code.
- Maybe "+A Size" to set the number of threads in async thread pool also helps a bit when do IO.
Practice
Steve and Anders have pushed widefinder in Erlang to 1.1 sec on 8-CPU linux box. Their code took about 1.9 sec on my 4-CPU box. Then, how about a concise version?
According to above guide, based on my previous code and Per's dict code, Luke's spawn_worker, I rewrote a concise and straightforward tbray5.erl (less than 80 LOC), without any extra c-drived modules for Tim's exercise , and got about 2.972 sec for 1 milli lines log file, and 15.695 sec for 5 milli lines, vs no-parallelized Ruby's 4.161 sec and 20.768 sec on my 2.80Ghz 4-CPU Intel Xeon linux box:
BTW, using ets instead of dict is almost the same.
$ erlc -smp tbray5.erl $ time erl +h 8192 -smp -noshell -run tbray5 start o1000k.ap -s erlang halt real 0m2.972s user 0m9.685s sys 0m0.748s $ time erl +h 8192 -smp -noshell -run tbray5 start o5000k.ap -s erlang halt real 0m15.695s user 0m53.551s sys 0m4.268s
On 2.0GHz 2-core MacBook (Ruby code took 2.447 sec):
$ time erl +h 8192 -smp -noshell -run tbray5 start o1000k.ap -s erlang halt real 0m3.034s user 0m4.853s sys 0m0.872s
The Code: tbray5.erl
-module(tbray5). -compile([native]). -export([start/1]). -define(BUFFER_SIZE, (1024 * 10000)). start(FileName) -> Dicts = [wait_result(Worker) || Worker <- read_file(FileName)], print_result(merge_dicts(Dicts)). read_file(FileName) -> {ok, File} = file:open(FileName, [raw, binary]), read_file_1(File, 0, []). read_file_1(File, Offset, Workers) -> case file:pread(File, Offset, ?BUFFER_SIZE) of eof -> file:close(File), Workers; {ok, Bin} -> DataL = split_on_last_newline(Bin), Worker = spawn_worker(self(), fun scan_chunk/1, {Bin, DataL}), read_file_1(File, Offset + DataL + 1, [Worker | Workers]) end. split_on_last_newline(Bin) -> split_on_last_newline_1(Bin, size(Bin)). split_on_last_newline_1(Bin, S) when S > 0 -> case Bin of <<_:S/binary,$\n,_/binary>> -> S; _ -> split_on_last_newline_1(Bin, S - 1) end; split_on_last_newline_1(_, S) -> S. scan_chunk({Bin, DataL}) -> scan_chunk_1(Bin, DataL, 0, dict:new()). scan_chunk_1(Bin, DataL, S, Dict) when S < DataL - 34 -> case Bin of <<_:S/binary,"GET /ongoing/When/",_,_,_,$x,$/,_,_,_,_,$/,_,_,$/,_,_,$/,_/binary>> -> case match_until_space_newline(Bin, S + 34) of {true, E} -> Skip = S + 23, L = E - Skip, <<_:Skip/binary,Key:L/binary,_/binary>> = Bin, scan_chunk_1(Bin, DataL, E + 1, dict:update_counter(Key, 1, Dict)); {false, E} -> scan_chunk_1(Bin, DataL, E + 1, Dict) end; _ -> scan_chunk_1(Bin, DataL, S + 1, Dict) end; scan_chunk_1(_, _, _, Dict) -> Dict. match_until_space_newline(Bin, S) when S < size(Bin) -> case Bin of <<_:S/binary,10,_/binary>> -> {false, S}; <<_:S/binary,$.,_/binary>> -> {false, S}; <<_:S/binary,_,$ ,_/binary>> -> {true, S + 1}; _ -> match_until_space_newline(Bin, S + 1) end; match_until_space_newline(_, S) -> {false, S}. spawn_worker(Parent, F, A) -> erlang:spawn_monitor(fun() -> Parent ! {self(), F(A)} end). wait_result({Pid, Ref}) -> receive {'DOWN', Ref, _, _, normal} -> receive {Pid, Result} -> Result end; {'DOWN', Ref, _, _, Reason} -> exit(Reason) end. merge_dicts([D1,D2|Rest]) -> merge_dicts([dict:merge(fun(_, V1, V2) -> V1 + V2 end, D1, D2) | Rest]); merge_dicts([D]) -> D. print_result(Dict) -> SortedList = lists:reverse(lists:keysort(2, dict:to_list(Dict))), [io:format("~b\t: ~p~n", [V, K]) || {K, V} <- lists:sublist(SortedList, 10)].
Tim's Erlang Exercise - Summary
>>> Updated Nov 1:
Tim tested my last attempt tbray5.erl, which was described on Learning Coding Binary (Was Tim's Erlang Exercise - Round VI), got for his 971,538,252 bytes of data in 4,625,236 lines log file:
real 0m20.74s user 3m51.33s sys 0m8.00s
It's not the fastest, since I did not apply Boyer-Moore searching. But it's what I want: a balance between simple, readable and speed.
========
>>> Updated Oct 24:
The Erlang code can be faster than un-parallelized Ruby, a new version run 2.97 sec on the 4-CPU box: Learning Coding Binary (Was Tim's Erlang Exercise - Round VI)
========
>>> Updated Oct 22:
As Bjorn's suggestion, I added "+h 4096" option for 'erl', which means "sets the default heap size of processes to the size 4096", the elapsed time dropped from 7.7s to 5.5s immediately:
time erl +h 4096 -smp -noshell -run tbray4 start o1000k.ap 10 -s erlang halt
The +h option seems to affect on binary version a lot, but few on list version. This may be caused by that list is always copied and binary may be left in process' heap and passed by point?
The default heap size for each process is set to 233 Word, this number may be suitable for a lot of concurrent processes to avoid too much memory exhaust. But for some parallelization tasks, with less processes, or with enough memory, the heap size can be adjusted to a bit large.
Anyway, I think Erlang/OTP has been very good there for Concurrency, but there may be still room to optimize for Parallelization.
BTW, with +h option, and some tips for efficient binary, the most concise binary version tbray5.erl can run into 3 sec now.
========
This is a performance summary on Tim's Erlang exercise on large dataset processing, I only compare the results on a 4-CPU Intel Xeon 2.80G linux box:
Log File | Time | Erlang(1 Proc) | Erlang(Many Proc) | Erlang(Many Proc) +h 4096 | Ruby |
1 milli lines | real | 22.088s | 7.700s | 5.475s | 4.161s |
user | 21.161s | 25.750s | 18.785s | 3.592s | |
sys | 0.924s | 3.552s | 1.352s | 0.568s | |
5 milli lines | real | 195.570s | 37.669s | 27.911s | 20.768s |
user | 192.496s | 126.296s | 98.162s | 19.009s | |
sys | 3.480s | 17.789s | 7.344s | 3.116s |
Notice:
- The Erlang code is tbray4.erl, which can be found in previous blog, the Ruby code is from Tim's blog.
- Erlang code is parallelized, Ruby code not.
- Erlang code is with tons of code, but, parallelization is not free lunch.
- With an 8-CPU box, Erlang's version should exceed or near to non-parallelized Ruby version*.
- Although we are talking about multiple-core era, but I'm not sure if disk/io is also ready.
* Per Steve's testing.
CN Erlounge II
It was last weekend, in Zhuhai, China, a two day CN Erlounge II, discussed topics of Erlang and FP:
- Why I choose Erlang? - by xushiwei
- Erlang emulator implementation - by mryufeng
- Port & driver - by codeplayer
- Py 2 Erl - by Zoom.Quiet
- STM: Lock free concurrent Overview - by Albert Lee
- mnesia - by mryufeng
And a new logo of "ERL.CHINA" was born as:
(Picture source - http://www.haokanbu.com/story/1104/)
More information about CN Erlounge II can be found here and some pictures.
I did not schedule for this meeting, maybe next time.
Learning Coding Parallelization (Was Tim's Erlang Exercise - Round V)
>>> Updated Oct 20:
After cleaned up my 4-CPU linux box, the result for the 1M records file is about 7.7 sec, for the 5M records file is about 38 sec.
========
>>> Updated Oct 16:
After testing my code on different machines, I found that disk/io performed varyingly, for some very large files, reading file in parallel may cause longer elapsed time (typically on non-server machine, which is not equipped for fast disk/io). So, I added another version tbray4b.erl, in this version, only reading file is not parallalized, all other code is the same. If you'd like to have a test on your machine, please try both.
========
Well, I think I've learned a lot from doing Tim's exercise, not only the List vs Binary in Erlang, but also computing in parallel. Coding Concurrency is farely easy in Erlang, but coding Parallelization is not only about the Languages, it's also a real question.
I wrote tbray3.erl in The Erlang Way (Was Tim Bray's Erlang Exercise - Round IV) and got a fairly good result by far on my 2-core MacBook. But things always are a bit complex. As Steve pointed in the comment, when he tried tbray3.erl on his 8-core linux box:
"I ran it in a loop 10 times, and the best time I saw was 13.872 sec, and user/CPU time was only 16.150 sec, so it’s apparently not using the multiple cores very well."
I also encoutered this issue on my 4-CPU Intel Xeon CPU 2.80GHz debian box, it runs even worse (8.420s) than my 2-core MacBook (4.483s).
I thought about my code a while, and found that my code seems spawning too many processes for scan_chunk, as the scan_chunk's performance has been improved a lot, each process will finish its task very quickly, too quick to the file reading, the inceasing CPUs have no much chance to play the game, the cycled 'reading'-'spawning scan process' is actually almost sequential now, there has been very few simultaneously alive scanning processes. I think I finally meet the file reading bound.
But wait, as I claimed before, that reading file to memory is very fast in Erlang, for a 200M log file, it takes less than 800ms. The time elapsed for tbray3.erl is about 4900ms, far away from 800ms, why I say the file reading is the bound now?
The problem here is: since I suspect the performance of traversing binary byte by byte, I choose to convert binary to list to scan the world. Per my testing results, list is better than binary when is not too longer, in many cases, not longer than several KBytes. And, to make the code clear and readable, I also choose splitting big binary when read file in the meanwhile, so, I have to read file in pieces of no longer than n KBytes. For a very big file, the reading procedure is broken to several ten-thousands steps, which finally cause the whole file reading time elapsed is bit long. That's bad.
So, I decide to write another version, which will read file in parallel (Round III), and split each chunk on lastest new-line (Round II), scan the words using pattern match (Round IV), and yes, I'll use binary instead of list this time, try to solve the worse performance of binary-traverse by parallel, on multiple cores.
The result is interesting, it's the first time I achieved around 10 sec in my 2-core MacBook when use binary match only, and it's also the first time, on my dummy 4-CPU Intel Xeon CPU 2.80GHz debian box, I got better result (7.700 sec) than my MacBook.
>>> Updated Oct 15:
Steve run the code on his 8-core 2.33 GHz Intel Xeon Linux box, with the best time was 4.920 sec:
"the best time I saw for your newest version was 4.920 sec on my 8-core Linux box. Fast! However, user time was only 14.751 sec, so I’m not sure it’s using all the cores that well. Perhaps you’re getting down to where I/O is becoming a more significant factor."
Please see Steve's One More Erlang Wide Finder and his widefinder attempts.
========
Result on 2.0GHz 2-core MacBook:
$ time erl -smp -noshell -run tbray4_bin start o1000k.ap 20 -s erlang halt 8900 : 2006/09/29/Dynamic-IDE 2000 : 2006/07/28/Open-Data 1300 : 2003/07/25/NotGaming 800 : 2003/10/16/Debbie 800 : 2003/09/18/NXML 800 : 2006/01/31/Data-Protection 700 : 2003/06/23/SamsPie 600 : 2006/09/11/Making-Markup 600 : 2003/02/04/Construction 600 : 2005/11/03/Cars-and-Office-Suites Time: 10527.50 ms real 0m10.910s user 0m13.927s sys 0m6.413s
Result on 4-CPU Intel Xeon CPU 2.80GHz debian box,:
# When process number is set to 20: time erl -smp -noshell -run tbray4_bin start o1000k.ap 20 -s erlang halt real 0m7.700s user 0m25.750s sys 0m3.552s # When process number is set to 1: $ time erl -smp -noshell -run tbray4_bin start o1000k.ap 1 -s erlang halt real 0m22.035s user 0m21.525s sys 0m0.512s # On a 940M 5 million lines log file: time erl -smp -noshell -run tbray4_bin start o5000k.ap 100 -s erlang halt 44500 : 2006/09/29/Dynamic-IDE 10000 : 2006/07/28/Open-Data 6500 : 2003/07/25/NotGaming 4000 : 2003/10/16/Debbie 4000 : 2003/09/18/NXML 4000 : 2006/01/31/Data-Protection 3500 : 2003/06/23/SamsPie 3000 : 2006/09/11/Making-Markup 3000 : 2003/02/04/Construction 3000 : 2005/11/03/Cars-and-Office-Suites Time: 37512.76 ms real 0m37.669s user 2m6.296s sys 0m17.789s
On the 4-CPU linux box, comparing the elapsed time between ProcNum = 20 and ProcNum = 1, the elapsed time of parallelized one was only 35% of un-parallelized one, speedup about 185%. The ratio was almost the same as my pread_file.erl testing on the same machine.
It's actually a combination of code in my four previous blogs. Although the performance is not so good as tbray3.erl on my MacBook, but I'm happy that this version is a fully parallelized one, from reading file, scanning words etc. it should scale better than all my previous versions.
-module(tbray4). -compile([native]). -export([start/1, start/2]). -include_lib("kernel/include/file.hrl"). start([FileName, ProcNum]) when is_list(ProcNum) -> start(FileName, list_to_integer(ProcNum)). start(FileName, ProcNum) -> Start = now(), Main = self(), Counter = spawn(fun () -> count_loop(Main) end), Collector = spawn(fun () -> collect_loop(Counter) end), pread_file(FileName, ProcNum, Collector), %% don't terminate, wait here, until all tasks done. receive stop -> io:format("Time: ~10.2f ms~n", [timer:now_diff(now(), Start) / 1000]) end. pread_file(FileName, ProcNum, Collector) -> ChunkSize = get_chunk_size(FileName, ProcNum), pread_file_1(FileName, ChunkSize, ProcNum, Collector). pread_file_1(FileName, ChunkSize, ProcNum, Collector) -> [spawn(fun () -> Length = if I == ProcNum - 1 -> ChunkSize * 2; %% lastest chuck true -> ChunkSize end, {ok, File} = file:open(FileName, [raw, binary]), {ok, Bin} = file:pread(File, ChunkSize * I, Length), file:close(File), {Data, Tail} = split_on_last_newline(Bin), Collector ! {seq, I, Data, Tail} end) || I <- lists:seq(0, ProcNum - 1)], Collector ! {chunk_num, ProcNum}. collect_loop(Counter) -> collect_loop_1([], <<>>, -1, Counter). collect_loop_1(Chunks, PrevTail, LastSeq, Counter) -> receive {chunk_num, ChunkNum} -> Counter ! {chunk_num, ChunkNum}, collect_loop_1(Chunks, PrevTail, LastSeq, Counter); {seq, I, Data, Tail} -> SortedChunks = lists:keysort(1, [{I, Data, Tail} | Chunks]), {Chunks1, PrevTail1, LastSeq1} = process_chunks(SortedChunks, [], PrevTail, LastSeq, Counter), collect_loop_1(Chunks1, PrevTail1, LastSeq1, Counter) end. count_loop(Main) -> count_loop_1(Main, dict:new(), undefined, 0). count_loop_1(Main, Dict, ChunkNum, ChunkNum) -> print_result(Dict), Main ! stop; count_loop_1(Main, Dict, ChunkNum, ProcessedNum) -> receive {chunk_num, ChunkNumX} -> count_loop_1(Main, Dict, ChunkNumX, ProcessedNum); {dict, DictX} -> Dict1 = dict:merge(fun (_, V1, V2) -> V1 + V2 end, Dict, DictX), count_loop_1(Main, Dict1, ChunkNum, ProcessedNum + 1) end. process_chunks([], ChunkBuf, PrevTail, LastSeq, _) -> {ChunkBuf, PrevTail, LastSeq}; process_chunks([{I, Data, Tail}=Chunk|T], ChunkBuf, PrevTail, LastSeq, Counter) -> case LastSeq + 1 of I -> spawn(fun () -> Counter ! {dict, scan_chunk(<<PrevTail/binary, Data/binary>>)} end), process_chunks(T, ChunkBuf, Tail, I, Counter); _ -> process_chunks(T, [Chunk | ChunkBuf], PrevTail, LastSeq, Counter) end. print_result(Dict) -> SortedList = lists:reverse(lists:keysort(2, dict:to_list(Dict))), [io:format("~b\t: ~s~n", [V, K]) || {K, V} <- lists:sublist(SortedList, 10)]. get_chunk_size(FileName, ProcNum) -> {ok, #file_info{size=Size}} = file:read_file_info(FileName), Size div ProcNum. split_on_last_newline(Bin) -> split_on_last_newline_1(Bin, size(Bin)). split_on_last_newline_1(Bin, Offset) when Offset > 0 -> case Bin of <<Data:Offset/binary,$\n,Tail/binary>> -> {Data, Tail}; _ -> split_on_last_newline_1(Bin, Offset - 1) end; split_on_last_newline_1(Bin, _) -> {Bin, <<>>}. scan_chunk(Bin) -> scan_chunk_1(Bin, 0, dict:new()). scan_chunk_1(Bin, Offset, Dict) when Offset =< size(Bin) - 34 -> case Bin of <<_:Offset/binary,"GET /ongoing/When/",_,_,_,$x,$/,Y1,Y2,Y3,Y4,$/,M1,M2,$/,D1,D2,$/,Rest/binary>> -> case match_until_space_newline(Rest, 0) of {Rest1, <<>>} -> scan_chunk_1(Rest1, 0, Dict); {Rest1, Word} -> Key = <<Y1,Y2,Y3,Y4,$/,M1,M2,$/,D1,D2,$/, Word/binary>>, scan_chunk_1(Rest1, 0, dict:update_counter(Key, 1, Dict)) end; _ -> scan_chunk_1(Bin, Offset + 1, Dict) end; scan_chunk_1(_, _, Dict) -> Dict. match_until_space_newline(Bin, Offset) when Offset < size(Bin) -> case Bin of <<Word:Offset/binary,$ ,Rest/binary>> -> {Rest, Word}; <<_:Offset/binary,$.,Rest/binary>> -> {Rest, <<>>}; <<_:Offset/binary,10,Rest/binary>> -> {Rest, <<>>}; _ -> match_until_space_newline(Bin, Offset + 1) end; match_until_space_newline(_, _) -> {<<>>, <<>>}.
>>> Updated Oct 16:
After testing my code on different machines, I found that disk/io performed varyingly, for some very large files, reading file in parallel may cause longer elapsed time (typically on non-server machine, which is not equipped for fast disk/io). So, I wrote another version: tbray4b.erl, in this version, only reading file is not parallalized, all other code is the same. Here's a result for this version on a 940M file with 5 million lines, with ProcNum set to 200 and 400)
# On 2-core MacBook: $ time erl -smp -noshell -run tbray4b start o5000k.ap 200 -s erlang halt real 0m50.498s user 0m49.746s sys 0m11.979s # On 4-cpu linux box: $ time erl -smp -noshell -run tbray4b start o5000k.ap 400 -s erlang halt real 1m2.136s user 1m59.907s sys 0m7.960s
The code: tbray4b.erl
-module(tbray4b). -compile([native]). -export([start/1, start/2]). -include_lib("kernel/include/file.hrl"). start([FileName, ProcNum]) when is_list(ProcNum) -> start(FileName, list_to_integer(ProcNum)). start(FileName, ProcNum) -> Start = now(), Main = self(), Counter = spawn(fun () -> count_loop(Main) end), Collector = spawn(fun () -> collect_loop(Counter) end), read_file(FileName, ProcNum, Collector), %% don't terminate, wait here, until all tasks done. receive stop -> io:format("Time: ~10.2f ms~n", [timer:now_diff(now(), Start) / 1000]) end. read_file(FileName, ProcNum, Collector) -> ChunkSize = get_chunk_size(FileName, ProcNum), {ok, File} = file:open(FileName, [raw, binary]), read_file_1(File, ChunkSize, 0, Collector). read_file_1(File, ChunkSize, I, Collector) -> case file:read(File, ChunkSize) of eof -> file:close(File), Collector ! {chunk_num, I}; {ok, Bin} -> spawn(fun () -> {Data, Tail} = split_on_last_newline(Bin), Collector ! {seq, I, Data, Tail} end), read_file_1(File, ChunkSize, I + 1, Collector) end. collect_loop(Counter) -> collect_loop_1([], <<>>, -1, Counter). collect_loop_1(Chunks, PrevTail, LastSeq, Counter) -> receive {chunk_num, ChunkNum} -> Counter ! {chunk_num, ChunkNum}, collect_loop_1(Chunks, PrevTail, LastSeq, Counter); {seq, I, Data, Tail} -> SortedChunks = lists:keysort(1, [{I, Data, Tail} | Chunks]), {Chunks1, PrevTail1, LastSeq1} = process_chunks(SortedChunks, [], PrevTail, LastSeq, Counter), collect_loop_1(Chunks1, PrevTail1, LastSeq1, Counter) end. count_loop(Main) -> count_loop_1(Main, dict:new(), undefined, 0). count_loop_1(Main, Dict, ChunkNum, ChunkNum) -> print_result(Dict), Main ! stop; count_loop_1(Main, Dict, ChunkNum, ProcessedNum) -> receive {chunk_num, ChunkNumX} -> count_loop_1(Main, Dict, ChunkNumX, ProcessedNum); {dict, DictX} -> Dict1 = dict:merge(fun (_, V1, V2) -> V1 + V2 end, Dict, DictX), count_loop_1(Main, Dict1, ChunkNum, ProcessedNum + 1) end. process_chunks([], ChunkBuf, PrevTail, LastSeq, _) -> {ChunkBuf, PrevTail, LastSeq}; process_chunks([{I, Data, Tail}=Chunk|T], ChunkBuf, PrevTail, LastSeq, Counter) -> case LastSeq + 1 of I -> spawn(fun () -> Counter ! {dict, scan_chunk(<<PrevTail/binary, Data/binary>>)} end), process_chunks(T, ChunkBuf, Tail, I, Counter); _ -> process_chunks(T, [Chunk | ChunkBuf], PrevTail, LastSeq, Counter) end. print_result(Dict) -> SortedList = lists:reverse(lists:keysort(2, dict:to_list(Dict))), [io:format("~b\t: ~s~n", [V, K]) || {K, V} <- lists:sublist(SortedList, 10)]. get_chunk_size(FileName, ProcNum) -> {ok, #file_info{size=Size}} = file:read_file_info(FileName), Size div ProcNum. split_on_last_newline(Bin) -> split_on_last_newline_1(Bin, size(Bin)). split_on_last_newline_1(Bin, Offset) when Offset > 0 -> case Bin of <<Data:Offset/binary,$\n,Tail/binary>> -> {Data, Tail}; _ -> split_on_last_newline_1(Bin, Offset - 1) end; split_on_last_newline_1(Bin, _) -> {Bin, <<>>}. scan_chunk(Bin) -> scan_chunk_1(Bin, 0, dict:new()). scan_chunk_1(Bin, Offset, Dict) when Offset =< size(Bin) - 34 -> case Bin of <<_:Offset/binary,"GET /ongoing/When/",_,_,_,$x,$/,Y1,Y2,Y3,Y4,$/,M1,M2,$/,D1,D2,$/,Rest/binary>> -> case match_until_space_newline(Rest, 0) of {Rest1, <<>>} -> scan_chunk_1(Rest1, 0, Dict); {Rest1, Word} -> Key = <<Y1,Y2,Y3,Y4,$/,M1,M2,$/,D1,D2,$/, Word/binary>>, scan_chunk_1(Rest1, 0, dict:update_counter(Key, 1, Dict)) end; _ -> scan_chunk_1(Bin, Offset + 1, Dict) end; scan_chunk_1(_, _, Dict) -> Dict. match_until_space_newline(Bin, Offset) when Offset < size(Bin) -> case Bin of <<Word:Offset/binary,$ ,Rest/binary>> -> {Rest, Word}; <<_:Offset/binary,$.,Rest/binary>> -> {Rest, <<>>}; <<_:Offset/binary,10,Rest/binary>> -> {Rest, <<>>}; _ -> match_until_space_newline(Bin, Offset + 1) end; match_until_space_newline(_, _) -> {<<>>, <<>>}.
=======
The Erlang Way (Was Tim Bray's Erlang Exercise - Round IV)
Playing with Tim's Erlang Exercise is so much fun.
I've been coding in Erlang about 6 months as a newbie, in most cases, I do parsing on string (or list what ever) with no need of regular expressions, since Erlang's pattern match can usaully solve most problems straightforward.
Tim's log file is also a good example for applying pattern match in Erlang way. It's a continuous stream of dataset, after splitting it to line-bounded chunks for parallellization purpose, we can truely match whole {GET /ongoing/When/\d\d\dx/(\d\d\d\d/\d\d/\d\d/[^ .]+) } directly on chunk with no need to split to lines any more.
This come out my third solution, which matchs whole
{GET /ongoing/When/\d\d\dx/(\d\d\d\d/\d\d/\d\d/[^ .]+) }
likeness using the pattern:
"GET /ongoing/When/"++[_,_,_,$x,$/,Y1,Y2,Y3,Y4,$/,M1,M2,$/,D1,D2,$/|Rest]
and then fetchs
[Y1,Y2,Y3,Y4,$/,M1,M2,$/,D1,D2,$/] ++ match_until_space_newline(Rest, [])
as the matched key, with no need to split the chunk to lines.
But yes, we still need to split each chunk on the lastest newline to get parallelized result exactly accurate.
On my 2-core 2 GHz MacBook, the best time I’ve got is 4.483 sec
# smp enabled: $ erlc -smp tbray3.erl $ time erl -smp +P 60000 -noshell -run tbray3 start o1000k.ap -s erlang halt 8900 : <<"2006/09/29/Dynamic-IDE">> 2000 : <<"2006/07/28/Open-Data">> 1300 : <<"2003/07/25/NotGaming">> 800 : <<"2003/10/16/Debbie">> 800 : <<"2003/09/18/NXML">> 800 : <<"2006/01/31/Data-Protection">> 700 : <<"2003/06/23/SamsPie">> 600 : <<"2006/09/11/Making-Markup">> 600 : <<"2003/02/04/Construction">> 600 : <<"2005/11/03/Cars-and-Office-Suites">> Time: 4142.83 ms real 0m4.483s user 0m5.804s sys 0m0.615s # no-smp: $ erlc tbray3.erl $ time erl -noshell -run tbray_list_no_line start o1000k.ap -s erlang halt real 0m7.050s user 0m6.183s sys 0m0.644s
The smp enable result speedup about 57%
On the 2.80GHz 4-cpu xeon debian box that I mentioned before in previous blog, the best result is:
real 0m8.420s user 0m11.637s sys 0m0.452s
And I've noticed, adjusting the BUFFER_SIZE can balance the time consumered by parallelized parts and un-parallelized parts. That is, if the number of core is increased, we can also increase the BUFFER_SIZE a bit, so the number of chunks decreased (less un-parallelized split_on_last_new_line/1 and file:pread/3) but with more heavy work for parallelized binary_to_list/1 and scan_chunk/1 on longer list.
The best BUFFER_SIZE on my computer is 4096 * 5 bytes, which causes un-parallized split_on_last_newline/1 took about only 0.226s in the case.
The code:
-module(tbray3). -compile([native]). -export([start/1]). %% The best Bin Buffer Size is 4096 * 1 - 4096 * 5 -define(BUFFER_SIZE, (4096 * 5)). start(FileName) -> Start = now(), Main = self(), Collector = spawn(fun () -> collect_loop(Main) end), {ok, File} = file:open(FileName, [raw, binary]), read_file(File, Collector), %% don't terminate, wait here, until all tasks done. receive stop -> io:format("Time: ~10.2f ms~n", [timer:now_diff(now(), Start) / 1000]) end. read_file(File, Collector) -> read_file_1(File, [], 0, Collector). read_file_1(File, PrevTail, I, Collector) -> case file:read(File, ?BUFFER_SIZE) of eof -> Collector ! {chunk_num, I}, file:close(File); {ok, Bin} -> {Chunk, NextTail} = split_on_last_newline(PrevTail ++ binary_to_list(Bin)), spawn(fun () -> Collector ! {dict, scan_chunk(Chunk)} end), read_file_1(File, NextTail, I + 1, Collector) end. split_on_last_newline(List) -> split_on_last_newline_1(lists:reverse(List), []). split_on_last_newline_1(List, Tail) -> case List of [] -> {lists:reverse(List), []}; [$\n|Rest] -> {lists:reverse(Rest), Tail}; [C|Rest] -> split_on_last_newline_1(Rest, [C | Tail]) end. collect_loop(Main) -> collect_loop_1(Main, dict:new(), undefined, 0). collect_loop_1(Main, Dict, ChunkNum, ChunkNum) -> print_result(Dict), Main ! stop; collect_loop_1(Main, Dict, ChunkNum, ProcessedNum) -> receive {chunk_num, ChunkNumX} -> collect_loop_1(Main, Dict, ChunkNumX, ProcessedNum); {dict, DictX} -> Dict1 = dict:merge(fun (_, V1, V2) -> V1 + V2 end, Dict, DictX), collect_loop_1(Main, Dict1, ChunkNum, ProcessedNum + 1) end. print_result(Dict) -> SortedList = lists:reverse(lists:keysort(2, dict:to_list(Dict))), [io:format("~b\t: ~p~n", [V, K]) || {K, V} <- lists:sublist(SortedList, 10)]. scan_chunk(List) -> scan_chunk_1(List, dict:new()). scan_chunk_1(List, Dict) -> case List of [] -> Dict; "GET /ongoing/When/"++[_,_,_,$x,$/,Y1,Y2,Y3,Y4,$/,M1,M2,$/,D1,D2,$/|Rest] -> case match_until_space_newline(Rest, []) of {Rest1, []} -> scan_chunk_1(Rest1, Dict); {Rest1, Word} -> Key = list_to_binary([Y1,Y2,Y3,Y4,$/,M1,M2,$/,D1,D2,$/, Word]), scan_chunk_1(Rest1, dict:update_counter(Key, 1, Dict)) end; [_|Rest] -> scan_chunk_1(Rest, Dict) end. match_until_space_newline(List, Word) -> case List of [] -> {[], []}; [10|_] -> {List, []}; [$.|_] -> {List, []}; [$ |_] -> {List, lists:reverse(Word)}; [C|Rest] -> match_until_space_newline(Rest, [C | Word]) end.
I also wrote another corresponding binary version, which is 2-3 times slower than above list version on my machine, but the result may vary depending on your compiled Erlang/OTP on various operation system. I will test it again when Erlang/OTP R12B is released, which is claimed to have been optimized for binary match performance.
-module(tbray3_bin). -compile([native]). -export([start/1]). -define(BUFFER_SIZE, (4096 * 10000)). start(FileName) -> Start = now(), Main = self(), Collector = spawn(fun () -> collect_loop(Main) end), {ok, File} = file:open(FileName, [raw, binary]), read_file(File, Collector), %% don't terminate, wait here, until all tasks done. receive stop -> io:format("Time: ~p ms~n", [timer:now_diff(now(), Start) / 1000]) end. collect_loop(Main) -> collect_loop_1(Main, dict:new(), undefined, 0). collect_loop_1(Main, Dict, ChunkNum, ChunkNum) -> print_result(Dict), Main ! stop; collect_loop_1(Main, Dict, ChunkNum, ProcessedNum) -> receive {chunk_num, ChunkNumX} -> collect_loop_1(Main, Dict, ChunkNumX, ProcessedNum); {dict, DictX} -> Dict1 = dict:merge(fun (_, V1, V2) -> V1 + V2 end, Dict, DictX), collect_loop_1(Main, Dict1, ChunkNum, ProcessedNum + 1) end. print_result(Dict) -> SortedList = lists:reverse(lists:keysort(2, dict:to_list(Dict))), [io:format("~b\t: ~s~n", [V, K]) || {K, V} <- lists:sublist(SortedList, 10)]. read_file(File, Collector) -> read_file_1(File, <<>>, 0, Collector). read_file_1(File, PrevTail, I, Collector) -> case file:read(File, ?BUFFER_SIZE) of eof -> file:close(File), Collector ! {chunk_num, I}; {ok, Bin} -> {Data, NextTail} = split_on_last_newline(Bin), spawn(fun () -> Collector ! {dict, scan_chunk(<<PrevTail/binary, Data/binary>>)} end), read_file_1(File, NextTail, I + 1, Collector) end. split_on_last_newline(Bin) -> split_on_last_newline_1(Bin, size(Bin)). split_on_last_newline_1(Bin, Offset) when Offset > 0 -> case Bin of <<Data:Offset/binary,$\n,Tail/binary>> -> {Data, Tail}; _ -> split_on_last_newline_1(Bin, Offset - 1) end; split_on_last_newline_1(Bin, _) -> {Bin, <<>>}. scan_chunk(Bin) -> scan_chunk_1(Bin, 0, dict:new()). scan_chunk_1(Bin, Offset, Dict) when Offset < size(Bin) - 34 -> case Bin of <<_:Offset/binary,"GET /ongoing/When/",_,_,_,$x,$/,Y1,Y2,Y3,Y4,$/,M1,M2,$/,D1,D2,$/,Rest/binary>> -> case match_until_space_newline(Rest, 0) of {Rest1, <<>>} -> scan_chunk_1(Rest1, 0, Dict); {Rest1, Word} -> Key = <<Y1,Y2,Y3,Y4,$/,M1,M2,$/,D1,D2,$/, Word/binary>>, scan_chunk_1(Rest1, 0, dict:update_counter(Key, 1, Dict)) end; _ -> scan_chunk_1(Bin, Offset + 1, Dict) end; scan_chunk_1(_, _, Dict) -> Dict. match_until_space_newline(Bin, Offset) when Offset < size(Bin) -> case Bin of <<Word:Offset/binary,$ ,Rest/binary>> -> {Rest, Word}; <<_:Offset/binary,$.,Rest/binary>> -> {Rest, <<>>}; <<_:Offset/binary,10,Rest/binary>> -> {Rest, <<>>}; _ -> match_until_space_newline(Bin, Offset + 1) end; match_until_space_newline(_, _) -> {<<>>, <<>>}.
Take a Break as Trader
Udated Dec 7: Well, I bought some stocks again today. Let's see year 2008.
I sold out all my hold on Stock Exchange of China this morning, and will take a break until end of this year. Wow, what a year.
Reading File in Parallel in Erlang (Was Tim Bray's Erlang Exercise - Round III)
My first solution for Tim's exercise tried to read file in parallel, but I just realized by reading file module's source code, that file:open(FileName, Options) will return a process instead of IO device. Well, this means a lot:
- It's a process, so, when you request more data on it, you actually send message to it. Since you only send 2 integer: the offset and length, sending message should be very fast. But then, this process (File) will wait for receiving data from disk/io. For one process, the receiving is sequential rather than parallelized.
- If we look the processes in Erlang as ActiveObjects, which send/receive messages/data in async, since the receiving is sequential in one process, requesting/waiting around one process(or, object) is almost safe for parallelized programming, you usaully do not need to worry about lock/unlock etc. (except the outside world).
- We can open a lot of File processes to read data in parallel, the bound is the disk/IO and the os' resources limit.
I wrote some code to test file reading in parallel, discardng the disk cache, on my 2-core MacBook, reading file with two processes can speedup near 200% to one process.
The code:
-module(file_pread). -compile([native]). -export([start/2]). -include_lib("kernel/include/file.hrl"). start(FileName, ProcNum) -> [start(FileName, ProcNum, Fun) || Fun <- [fun read_file/3, fun pread_file/3]]. start(FileName, ProcNum, Fun) -> Start = now(), Main = self(), Collector = spawn(fun () -> collect_loop(Main) end), Fun(FileName, ProcNum, Collector), %% don't terminate, wait here, until all tasks done. receive stop -> io:format("time: ~10.2f ms~n", [timer:now_diff(now(), Start) / 1000]) end. collect_loop(Main) -> collect_loop_1(Main, undefined, 0). collect_loop_1(Main, ChunkNum, ChunkNum) -> Main ! stop; collect_loop_1(Main, ChunkNum, ProcessedNum) -> receive {chunk_num, ChunkNumX} -> collect_loop_1(Main, ChunkNumX, ProcessedNum); {seq, _Seq} -> collect_loop_1(Main, ChunkNum, ProcessedNum + 1) end. get_chunk_size(FileName, ProcNum) -> {ok, #file_info{size=Size}} = file:read_file_info(FileName), Size div ProcNum. read_file(FileName, ProcNum, Collector) -> ChunkSize = get_chunk_size(FileName, ProcNum), {ok, File} = file:open(FileName, [raw, binary]), read_file_1(File, ChunkSize, 0, Collector). read_file_1(File, ChunkSize, I, Collector) -> case file:read(File, ChunkSize) of eof -> file:close(File), Collector ! {chunk_num, I}; {ok, _Bin} -> Collector ! {seq, I}, read_file_1(File, ChunkSize, I + 1, Collector) end. pread_file(FileName, ProcNum, Collector) -> ChunkSize = get_chunk_size(FileName, ProcNum), pread_file_1(FileName, ChunkSize, ProcNum, Collector). pread_file_1(FileName, ChunkSize, ProcNum, Collector) -> [spawn(fun () -> %% if it's the lastest chuck, read all bytes left, %% which will not exceed ChunkSize * 2 Length = if I == ProcNum - 1 -> ChunkSize * 2; true -> ChunkSize end, {ok, File} = file:open(FileName, [raw, binary]), {ok, _Bin} = file:pread(File, ChunkSize * I, Length), Collector ! {seq, I}, file:close(File) end) || I <- lists:seq(0, ProcNum - 1)], Collector ! {chunk_num, ProcNum}.
The pread_file/3 is parallelized, it always opens new File process for each reading process instead of sharing one opened File process during all reading processes. The read_file/3 is non-parallelized.
To evaulate: (run at least two-time for each test to average disk/IO caches.)
$ erlc -smp file_pread.erl $ erl -smp 1> file_pread:start("o100k.ap", 2). time: 691.72 ms time: 44.37 ms [ok,ok] 2> file_pread:start("o100k.ap", 2). time: 74.50 ms time: 43.59 ms [ok,ok] 3> file_pread:start("o1000k.ap", 2). time: 1717.68 ms time: 408.48 ms [ok,ok] 4> file_pread:start("o1000k.ap", 2). time: 766.00 ms time: 393.71 ms [ok,ok] 5>
Let's compare the results for each file (we pick the second testing result of each), the speedup:
- o100k.ap, 20M, 74.50 / 43.59 - 1= 70%
- o1000k.ap, 200M, 766.00 / 393.71 - 1 = 95%
On another 4-CPU debian machine, with 4 processes, the best result I got:
4> file_pread:start("o1000k.ap", 4). time: 768.59 ms time: 258.57 ms [ok, ok] 5>
The parallelized reading speedup 768.59 / 258.57 -1 = 197%
I've updated my first solution according to this testing, opening new File process for each reading process instead of sharing the same File process. Of cource, there are still issues that I pointed in Tim Bray's Erlang Exercise on Large Dataset Processing - Round II
Although the above result can also be achieved in other Languages, but I find that coding parallelization in Erlang is a pleasure.
Tim Bray's Erlang Exercise on Large Dataset Processing - Round II
Updated Oct 09: Added more benchmark results under linux on other machines.
Updated Oct 07: More concise code.
Updated Oct 06: Fixed bugs: 1. Match "GET /ongoing/When/" instead of "/ongoing/When/"; 2. split_on_last_newline should not reverse Tail.
Backed from a short vacation, and sit down in front of my computer, I'm thinking about Tim Bray's exercise again.
As I realized, the most expensive procedure is splitting dataset to lines. To get the multiple-core benefit, we should parallelize this procedure instead of reading file to binary or macthing process only.
In my previous solution, there are at least two issues:
- Since the file reading is fast in Erlang, then, parallelizing the file reading is not much helpful.
- The buffered_read actually can be merged with the buffered file reading.
And, Per's solution parallelizes process_match procedure only, based on a really fast divide_to_lines, but with hacked binary matching syntax.
After a couple of hours working, I finially get the second version of tbray.erl (with some code from Per's solution).
- Read file to small pieces of binary (about 4096 bytes each chunk), then convert to list.
- Merge the previous tail for each chunk, search this chunk from tail, find the last new line mark, split this chunk to line-bounded data part, and tail part for next chunk.
- The above steps are difficult to parallelize. If we try, there will be about 30 more LOC, and not so readable.
- Spawn a new process at once to split line-bounded chunk to lines, process match and update dict.
- Thus we can go on reading file with non-stop.
- A collect_loop will receive dicts from each process, and merge them.
What I like of this version is, it scales on mutiple-core almost linearly! On my 2.0G 2-core MacBook, it took about 13.522 seconds with non-smp, 7.624 seconds with smp enabled (for a 200M data file, with about 50,000 processes spawned). The 2-core smp result achieves about 77% faster than non-smp result. I'm not sure how will it achieve on an 8-core computer, but we'll finally reach the limit due to the un-parallelized procedures.
The Erlang time results:
$ erlc tbray.erl $ time erl -noshell -run tbray start o1000k.ap -s erlang halt > /dev/null real 0m13.522s user 0m12.265s sys 0m1.199s $ erlc -smp tbray.erl $ time erl -smp +P 60000 -noshell -run tbray start o1000k.ap -s erlang halt > /dev/null real 0m7.624s user 0m13.302s sys 0m1.602s # For 5 million lines, 958.4M size: $ time erl -smp +P 300000 -noshell -run tbray start o5000k.ap -s erlang halt > /dev/null real 0m37.085s user 1m5.605s sys 0m7.554s
And the original Tim's Ruby version:
$ time ruby tbray.rb o1000k.ap > /dev/null real 0m2.447s user 0m2.123s sys 0m0.306s # For 5 million lines, 958.4M size: $ time ruby tbray.rb o5000k.ap > /dev/null real 0m12.115s user 0m10.494s sys 0m1.473s
Erlang time result on 2-core 1.86GHz CPU RedHat linux box, with kernel:
Linux version 2.6.18-1.2798.fc6 (brewbuilder@hs20-bc2-4.build.redhat.com) (gcc v
ersion 4.1.1 20061011 (Red Hat 4.1.1-30)) #1 SMP Mon Oct 16 14:37:32 EDT 2006
is 7.7 seconds.
Erlang time result on 2.80GHz 4-cpu xeon debian box, with kernel:
Linux version 2.6.15.4-big-smp-tidy (root@test) (gcc version 4.0.3 20060128 (prerelease) (Debian 4.0
.2-8)) #1 SMP Sat Feb 25 21:24:23 CST 2006
The smp result on this 4-cpu computer is questionable. It speededup only 50% than non-smp, even worse than my 2.0GHz 2-core MacBook. I also tested the Big Bang on this machine, it speedup less than 50% too.
$ erlc tbray.erl $ time erl -noshell -run tbray start o1000k.ap -s erlang halt > /dev/null real 0m22.279s user 0m21.597s sys 0m0.676s $ erlc -smp tbray.erl $ time erl -smp +S 4 +P 60000 -noshell -run tbray start o1000k.ap -s erlang halt > /dev/null real 0m14.765s user 0m28.722s sys 0m0.840s
Notice:
- All tests run several times to have the better result expressed, so, the status of disk/io cache should be near.
- You may need to compile tbray.erl to two different BEAMs, one for smp version, and one for no-smp version.
- If you'd like to process bigger file, you can use +P processNum to get more simultaneously alive Erlang processes. For BUFFER_SIZE=4096, you can set +P arg as FileSize / 4096, or above. From Erlang's Efficiency Guide:
Processes
The maximum number of simultaneously alive Erlang processes is by default 32768. This limit can be raised up to at most 268435456 processes at startup (see documentation of the system flag +P in the erl(1) documentation). The maximum limit of 268435456 processes will at least on a 32-bit architecture be impossible to reach due to memory
To evaluate with smp enable: (Erlang/OTP R11B-5 for Windows may not support smp yet)
erl -smp +P 60000 > tbray:start("o1000k.ap").
The code: (pretty formatted by ErlyBird 0.15.1)
-module(tbray_blog). -compile([native]). -export([start/1]). %% The best Bin Buffer Size is 4096 -define(BUFFER_SIZE, 4096). start(FileName) -> Start = now(), Main = self(), Collector = spawn(fun () -> collect_loop(Main) end), {ok, File} = file:open(FileName, [raw, binary]), read_file(File, Collector), %% don't terminate, wait here, until all tasks done. receive stop -> io:format("Time: ~10.2f ms~n", [timer:now_diff(now(), Start) / 1000]) end. read_file(File, Collector) -> read_file_1(File, [], 0, Collector). read_file_1(File, PrevTail, I, Collector) -> case file:read(File, ?BUFFER_SIZE) of eof -> Collector ! {chunk_num, I}, file:close(File); {ok, Bin} -> {Data, NextTail} = split_on_last_newline(PrevTail ++ binary_to_list(Bin)), spawn(fun () -> Collector ! {dict, scan_lines(Data)} end), read_file_1(File, NextTail, I + 1, Collector) end. split_on_last_newline(List) -> split_on_last_newline_1(lists:reverse(List), []). split_on_last_newline_1(List, Tail) -> case List of [] -> {lists:reverse(List), []}; [$\n|Rest] -> {lists:reverse(Rest), Tail}; [C|Rest] -> split_on_last_newline_1(Rest, [C | Tail]) end. collect_loop(Main) -> collect_loop_1(Main, dict:new(), undefined, 0). collect_loop_1(Main, Dict, ChunkNum, ChunkNum) -> print_result(Dict), Main ! stop; collect_loop_1(Main, Dict, ChunkNum, ProcessedNum) -> receive {chunk_num, ChunkNumX} -> collect_loop_1(Main, Dict, ChunkNumX, ProcessedNum); {dict, DictX} -> Dict1 = dict:merge(fun (_, V1, V2) -> V1 + V2 end, Dict, DictX), collect_loop_1(Main, Dict1, ChunkNum, ProcessedNum + 1) end. print_result(Dict) -> SortedList = lists:reverse(lists:keysort(2, dict:to_list(Dict))), [io:format("~p\t: ~s~n", [V, K]) || {K, V} <- lists:sublist(SortedList, 10)]. scan_lines(List) -> scan_lines_1(List, [], dict:new()). scan_lines_1(List, Line, Dict) -> case List of [] -> match_and_update_dict(lists:reverse(Line), Dict); [$\n|Rest] -> scan_lines_1(Rest, [], match_and_update_dict(lists:reverse(Line), Dict)); [C|Rest] -> scan_lines_1(Rest, [C | Line], Dict) end. match_and_update_dict(Line, Dict) -> case process_match(Line) of false -> Dict; {true, Word} -> dict:update_counter(Word, 1, Dict) end. process_match(Line) -> case Line of [] -> false; "GET /ongoing/When/"++[_,_,_,$x,$/,Y1,Y2,Y3,Y4,$/,M1,M2,$/,D1,D2,$/|Rest] -> case match_until_space(Rest, []) of [] -> false; Word -> {true, [Y1,Y2,Y3,Y4,$/,M1,M2,$/,D1,D2,$/] ++ Word} end; [_|Rest] -> process_match(Rest) end. match_until_space(List, Word) -> case List of [] -> []; [$.|_] -> []; [$ |_] -> lists:reverse(Word); [C|Rest] -> match_until_space(Rest, [C | Word]) end.
Lessons learnt:
- Split large binary to proper size chunks, then convert to list for further processing
- Parallelize the most expensive part (of course)
- We need a new or more complete Efficent Erlang
ErlyBird 0.15.1 Released - An Erlang IDE based on NetBeans
I'm pleased to announce ErlyBird 0.15.1, an Erlang IDE based on NetBeans. This is a performance improvement release. This release will only provide all-in-one IDE package, which is in size of 18.3M.
CHANGELOG:
- Performance improvement.
- Integrated with NetBeans' Common Scripting Framework. Thanks Tor.
- Fix a bug related to occurrences mark on built-in functions.
- Fix bug of wrong formatting multiple-lines string.
- Supports "-module(x.y.z)" syntax.
- Various bugs fixes.
Java JRE 5.0+ is required.
To download, please go to: http://sourceforge.net/project/showfiles.php?group_id=192439
To install:
- Unzip erlybird-bin-0.15.1-ide.zip to somewhere.
- Make sure 'erl.exe' or 'erl' is under your environment path
- For Windows user, execute 'bin/erlybird.exe'. For *nix user, 'bin/erlybird'.
- Check/set your OTP path. From [Tools]->[Options], click on 'Erlang', then 'Erlang Installation' tab, fill in the full path of your 'erl.exe' or 'erl' file. For instance: "C:/erl/bin/erl.exe"
- The default -Xmx option for jvm is set to 256M, ErlyBird now works good with less memory, such as -Xmx128M. If you want to increase/decrease it, please open the config file that is located at etc/erlybird.conf, set -J-Xmx of 'default_options'.
When run ErlyBird first time, the OTP libs will be indexed. The indexing time varies from 10 to 30 minutes deponding on your computer.
Notice:
If you have previous version of ErlyBird 0.12.0+ installed, you can keep your old cache files, otherwise, please delete the old cache files which are located at:
- *nix: "${HOME}/.erlybird/dev"
- mac os x: "${HOME}/Library/Application Support/erlybird/dev"
- windows: "C:\Documents and Settings\yourusername\.erlybird\dev" or some where
The status of ErlyBird is still Alpha, feedbacks and bug reports are welcome.
Tim Bray's Erlang Exercise on Large Dataset Processing
Updated Oct 10: pread_file/5 should open a new FIle process each cycle
Updated Oct 05: Wrote a new version, which is more parallelized-likeness.
Updated Sep 27: Per Gustafsson gave a better solution, it took 5.155 seconds on my computer, with all record maching tasks done. And, get all lines ready without record matching only took 1.58 seconds.
Updated Sep 27: The best result on my computer is 5.188 seconds, after added -compile([native])., with: tbray:start(20, "o1000k.ap").
Tim's Wide Finder Project tried Erlang on large log file (around 200M):
Where, the Erlang code took more than 30 seconds to fetch the matched records.
I'm with a lot of interesting on Erlang's efficiency to process large dataset, so I tried several testing, and got some conclutions.
First, the file io itself in Erlang is reasonable fast, to read a 200M file into memory, using file:read_file/1 took less than 800ms on my computer.
But, to process the dataset, you can not avoid to travel the whole dataset, to find newline marks, to match the express etc.
I wrote a piece of code to travel the whole dataset in binary form, as simple as just count it byte by byte. Well, it took about 35s for a 200M file, seems travel a binary byte by byte is the major cost.
Thomas gave some hints on how to travel a binary efficiently, here.
And Bjorn from OTP term gave some hints too.
Yes, the most efficient data struct in Erlang to travel dataset byte by byte is List.
Let's take some testing:
travel_bin(Bin) -> travel_bin(Bin, 0). travel_bin(<<>>, ByteCount) -> ByteCount; travel_bin(<<$\n, Rest/binary>>, ByteCount) -> travel_bin(Rest, ByteCount + 1); travel_bin(<<_C, Rest/binary>>, ByteCount) -> travel_bin(Rest, ByteCount + 1). travel_list(List) -> travel_list(List, 0). travel_list([], CharCount) -> CharCount; travel_list([$\n|Rest], CharCount) -> travel_list(Rest, CharCount + 1); travel_list([_C|Rest], CharCount) -> travel_list(Rest, CharCount + 1).
When apply to a 20M file, we got:
> {ok, Bin} = file:read_file("o100k.ap"). {ok,<<"host-24-225-218-245.patmedia.net - - [01/Oct/2006:06:33:45 -0700] \"GET /ongoing/ongoing.atom HTTP/1.1\" 304 -"...>>} > timer:tc(tbray, travel_bin, [Bin]). {2787402,20099550} > timer:tc(tbray, travel_list, [binary_to_list(Bin)]). {370906,20099550}
(Updated Oct 7: The statements about travel_list below are not quite accurate, the test of travel_list actually did not include the time taken by binary_to_list. The story of "Binary vs List in Erlang" is bit complex, the results vary depending on data size a lot. I'll post another article talking about it)
Where, travel_bin took about 2787.402ms, and travel_list took about 370.906ms (including the time costed to apply binary_to_list).
Pretty good result for travel_list, which was about 13% time costed comparing to travel_bin.
But, List is memory eater than Binary. Yes, when you try to apply above code to a file with 200M size, scene changes a lot:
> f(Bin). ok > {ok, Bin} = file:read_file("o1000k.ap"). {ok,<<"host-24-225-218-245.patmedia.net - - [01/Oct/2006:06:33:45 -0700] \"GET /ongoing/ongoing.atom HTTP/1.1\" 304 -"...>>} > timer:tc(tbray, travel_bin, [Bin]). {35414374,200995500} > timer:tc(tbray, travel_list, [binary_to_list(Bin)]). beam.smp(25965,0x1806400) malloc: *** vm_allocate(size=1782579200) failed (error code=3) ...
Where, size of o1000k.ap is about 200M. travel_bin took 35s, travel_list crashed.
How about split large binary to pieces, then convert them to lists, and travel them?
I tried, and, it's a bit trick. The trick is the buffer size. At first, I split binary to pieces of 1024 * 1024 size, the performance was even worse. I almost dropped. But, I tried more, when I adjusted the buffer size to 4096, this solution shines.
And finally, with a parallel file reader, I got an answer to Tim's exercise, plus a simple express matching, for an 1 million lines file (200M size), is 8.311 seconds when -smp enable, and 10.206 seconds when smp disabled.
My computer is a 2.0G 2-core MacBook, I'd like a see a result on more-core machine :-)
The code:
-module(tbray). -compile([native]). -export([start/2, collect_loop/2, buffered_read/3]). -include_lib("kernel/include/file.hrl"). %% The best BUFFER_SIZE is 4096 -define(BUFFER_SIZE, 4096). -record(context, {lineBuf = [], matched = 0, total = 0, lastProcessedSeq = 0, dataBuf = [], processNum}). %% erl -smp enable +P 60000 %% timer:tc(wide, start, [1000, "o1000k.ap"]). start(ProcessNum, FileName) -> statistics(wall_clock), {ok, FileInfo} = file:read_file_info(FileName), Size = FileInfo#file_info.size, Collect = spawn(?MODULE, collect_loop, [self(), #context{processNum = ProcessNum}]), psplit_read_file(Collect, FileName, Size div ProcessNum, ProcessNum, 1), {Matched, Total} = receive #context{matched=MatchedX, total=TotalX} -> {MatchedX, TotalX} end, {_, Duration2} = statistics(wall_clock), io:format("scan lines:\t ~pms~nMatched: ~B, Total: ~B~n", [Duration2, Matched, Total]). psplit_read_file(_Collector, _FileName, _ChunkSize, ProcessNum, I) when I > ProcessNum -> done; psplit_read_file(Collector, FileName, ChunkSize, ProcessNum, I) -> spawn( fun () -> Offset = ChunkSize * (I - 1), %% if it's last chuck, read all bytes left, which will not exceed ChunkSize * 2 Length = if I == ProcessNum -> ChunkSize * 2; true -> ChunkSize end, {ok, File} = file:open(FileName, [read, binary]), {ok, Data} = file:pread(File, Offset, Length), Collector ! {I, Data} end), psplit_read_file(Collector, FileName, ChunkSize, ProcessNum, I + 1). collect_loop(Pid, #context{lastProcessedSeq= ProcessNum, processNum=ProcessNum}=Context) -> Pid ! Context; collect_loop(Pid, #context{dataBuf=DataBuf}=Context) -> receive {Seq, Data} -> SortedDatas = lists:keysort(1, [{Seq, Data} | DataBuf]), Context1 = process_arrived_datas(SortedDatas, Context#context{dataBuf = []}), %io:format("Last processed Seq: ~B~n", [Context1#context.lastProcessedSeq]), collect_loop(Pid, Context1) end. process_arrived_datas([], Context) -> Context; process_arrived_datas([{Seq, Data}|T], #context{lineBuf=LineBuf, matched=Matched, total=Total, lastProcessedSeq=LastProcessedSeq, dataBuf=DataBuf}=Context) -> if Seq == LastProcessedSeq + 1 -> {LineBuf1, Matched1, Total1} = buffered_read( fun (Buffer, {LineBufX, MatchedX, TotalX}) -> scan_line(binary_to_list(Buffer), LineBufX, MatchedX, TotalX) end, {LineBuf, Matched, Total}, Data), process_arrived_datas(T, Context#context{lineBuf = LineBuf1, matched = Matched1, total = Total1, lastProcessedSeq = Seq}); true -> process_arrived_datas(T, Context#context{dataBuf = [{Seq, Data} | DataBuf]}) end. buffered_read(Fun, Acc, Bin) -> case Bin of <<Buf:?BUFFER_SIZE/binary, Rest/binary>> -> Acc1 = Fun(Buf, Acc), buffered_read(Fun, Acc1, Rest); _ -> Fun(Bin, Acc) end. scan_line([], LineBuf, Matched, Total) -> {LineBuf, Matched, Total}; scan_line([$\n|Rest], LineBuf, Matched, Total) -> Line1 = lists:reverse(LineBuf), %io:format("~n~s~n", [Line1]), Matched1 = Matched + process_match(Line1), scan_line(Rest, [], Matched1, Total + 1); scan_line([C|Rest], LineBuf, Matched, Total) -> scan_line(Rest, [C | LineBuf], Matched, Total). process_match([]) -> 0; process_match("GET /ongoing/When/"++Rest) -> case match_until_space(Rest, false) of true -> 0; false -> 1 end; process_match([_H|Rest]) -> process_match(Rest). match_until_space([$\040|_Rest], Bool) -> Bool; match_until_space([$.|_Rest], _Bool) -> true; match_until_space([_H|Rest], Bool) -> match_until_space(Rest, Bool).
Some hints:
The solution spawns a lot of processes to read the file to binary in parallel. Then send them to a collect_loop, collect_loop will buffered_read each chunk (when chunks order is correct), buffered_read then converts each binary to small (4096 bytes here) lists, the scan_line will merge them to lines, and process_match on line.
As I mentioned before, handle a short string line in Erlang is fast, so I do not fork process_match to processes.
The code can handle very large files.
The matching code may not be correct, and does not finish all tasks that Tim wants.
ErlyBird 0.15.0 Released - An Erlang IDE based on NetBeans
Updated(Sep 23): Known issues:
- Formmater: String literal that is broken to multiple lines will be reformatted, this should be fixed. (Done in trunk)
- 'Run project' does not work yet.
- When more than one projects are opened concurrently, 'Go to declaration' for remote function may not work. You can close other projects when encountered.
I'm pleased to announce ErlyBird 0.15.0, an Erlang IDE based on NetBeans. This is a major features release. This release will only provide all-in-one IDE package, which is in size of 17.6M.
CHANGELOG:
- Pretty formatter (Ctrl+Shift+F).
- Variables and functions occurrences mark.
- Better brace matching highlighting, such as for 'try-catch-end', 'if-end' etc.
- "-import" syntax now works in all cases, that means RabbitMQ's code will be parsed correctly.
- Various bugs fixes.
As NetBeans 6.0 beta1 was just released, I hope ErlyBird has got more stable also.
Java JRE 5.0+ is required.
To download, please go to: http://sourceforge.net/project/showfiles.php?group_id=192439
To install:
- Unzip erlybird-bin-0.15.0-ide.zip to somewhere.
- Make sure 'erl.exe' or 'erl' is under your environment path
- For Windows user, execute 'bin/erlybird.exe'. For *nix user, 'bin/erlybird'.
- Check/set your OTP path. From [Tools]->[Options], click on 'Erlang', then 'Erlang Installation' tab, fill in the full path of your 'erl.exe' or 'erl' file. For instance: "C:/erl/bin/erl.exe"
- The default -Xmx option for jvm is set to 256M, ErlyBird now works good with less memory, such as -Xmx128M. If you want to increase/decrease it, please open the config file that is located at etc/erlybird.conf, set -J-Xmx of 'default_options'.
When run ErlyBird first time, the OTP libs will be indexed. The indexing time varies from 10 to 30 minutes deponding on your computer.
Notice:
If you have previous version of ErlyBird 0.12.0+ installed, you can keep your old cache files, otherwise, please delete the old cache files which are located at:
- *nix: "${HOME}/.erlybird/dev"
- mac os x: "${HOME}/Library/Application Support/erlybird/dev"
- windows: "C:\Documents and Settings\yourusername\.erlybird\dev" or some where
The status of ErlyBird is still Alpha, feedbacks and bug reports are welcome.
ErlyBird Screenshot: Brace Matching and Mark Occurences
I patched NetBeans' Generic Languages Framework: Schliemann, and got Brace Matching and Mark Occurences working perfectly on ErlyBird now. As shown on the screenshot, when you put the caret on "end" or "case", the matched "case" or "end' will be highlighing. When you put the caret on a variable, for example, "State", the occurrences of the variable will be highlighting, and, on the right side bar, there are colored indicators telling you where are these occurrences. The occurrences marking also works for function names.
The coming release also fixed some bugs, such as the "import" syntax now works in all cases, that means RabbitMQ's code will be parsed correctly.
Click on the picture to enlarge it
From Rails to Erlyweb - Part II Manage Project - Reloaded
The migrating from Rails to Erlyweb of our project is going to be finished. I got more experience on how to deal with Erlyweb. First, the project management can be more straightforward. Here is it:
2. Manage project - Reloaded
Erlyweb provides erlyweb:compile(App, ..) to compile the source files under app directory. To start an app, you usually should erlydb:start(mysql, ....) and compile app files first. To make life easy, you can put some scripting like code under myproject\script directory. Here's my project source tree:
myproject + apps | + myapp | + ebin | + include | + nbproject | + src | + components | + lib | + services | + test | + www + config | * yaws.conf | * erlyweb.conf + script + ebin + src * erlyweb_app.erl
Where, config/yaws.conf contains the yaws' configuration. Here's mine:
ebin_dir = D:/myapp/trunk/script/ebin <server localhost> port = 8000 listen = 0.0.0.0 docroot = D:/myapp/trunk/apps/myapp/www appmods = </myapp, erlyweb> start_mod = erlyweb_app <opaque> appname = myapp environment = development </opaque> </server>
You may have noticed, all beams under D:/myapp/trunk/script/ebin will be auto-loaded when yaws starts up. And you can prepare another yaws.conf for test or production environment by change the environment var in opaque
Now the config/erlyweb.conf:
{pa, ["script/ebin", "apps/myapp/ebin", "vendor/erlyweb/ebin", "vendor/eunit/ebin"]}. {i, ["vendor", "apps/myapp/include", "/usr/local/lib/yaws"]}. {production, [{dbdriver, mysql}, {database, "mydb_production"}, {hostname, "localhost"}, {username, "mememe"}, {password, "pwpwpw"}]}. {development, [{dbdriver, mysql}, {database, "mydb_development"}, {hostname, "localhost"}, {username, "mememe"}, {password, "pwpwpw"}]}. {test, [{dbdriver, mysql}, {database, "mydb_test"}, {hostname, "localhost"}, {username, "mememe"}, {password, "pwpwpw"}]}.
erlyweb_app.erl is the boot scripting code, which will be used to start db connection and compile the code. Currently I run these scripts manually. I'll talk later.
Notice: erlyweb 0.6.2 needed, which contains Haoboy's logfun patch.
%% @doc Main entrance to the entire erlyweb application. -module(erlyweb_app). -export([start/1]). -export([get_conf/1, build/1, build_test/1, build_product/1, environment/1, decompile/2, db_log/4, db_dummy_log/4 ]). -include("yaws/include/yaws.hrl"). -include("yaws/include/yaws_api.hrl"). db_log(Module, Line, Level, FormatFun) -> mysql:log(Module, Line, Level, FormatFun). db_dummy_log(_Mod, _Line, _Level, _FormatFun) -> empty. %% @doc call back function when yaws start an app %% @see man yaws.conf %% start_mod = Module %% Defines a user provided callback module. At startup of the %% server, Module:start/1 will be called. The #sconf{} record %% (defined in yaws.hrl) will be used as the input argument. This %% makes it possible for a user application to syncronize the %% startup with the yaws server as well as getting hold of user %% specific configuration data, see the explanation for the %%context. start(SConf) -> Opaque = SConf#sconf.opaque, AppName = proplists:get_value("appname", Opaque), Environment = list_to_atom(proplists:get_value("environment", Opaque)), {_I, Pa, Pz, Dbdriver, Database, Hostname, Username, Password} = get_conf(Environment), {ok, Cwd} = file:get_cwd(), error_logger:info_msg("CWD: ~s~n", [Cwd]), add_code_path(Pa, Pz), LogFun = case Environment of undefined -> fun erlyweb_app:db_log/4; production -> fun erlyweb_app:db_dummy_log/4; development -> %code:add_pathz("../apps/ewp/src/test"), fun erlyweb_app:db_log/4; test -> fun erlyweb_app:db_log/4 end, error_logger:info_msg("Starting app <~s> as <~s> using database <~s>~n", [AppName, Environment, Database]), start_db(Dbdriver, Database, Hostname, Username, Password, LogFun). add_code_path(Pa, Pz) -> AddedPa = [{Dir, code:add_patha(Dir)} || Dir <- Pa], AddedPz = [{Dir, code:add_pathz(Dir)} || Dir <- Pz], error_logger:info_msg("Add code patha: ~p~n", [AddedPa]), error_logger:info_msg("Add code pathz: ~p~n", [AddedPz]). get_conf(Environment) when is_list(Environment) -> get_conf(list_to_atom(Environment)); get_conf(Environment) when is_atom(Environment) -> {ok, Confs} = file:consult("config/erlyweb.conf"), I = case proplists:get_value(i, Confs) of undefined -> []; IX -> IX end, Pa = case proplists:get_value(pa, Confs) of undefined -> []; PaX -> PaX end, Pz = case proplists:get_value(pz, Confs) of undefined -> []; PzX -> PzX end, EnvConfs = proplists:get_value(Environment, Confs), Dbdriver = proplists:get_value(dbdriver, EnvConfs), Database = proplists:get_value(database, EnvConfs), Hostname = proplists:get_value(hostname, EnvConfs), Username = proplists:get_value(username, EnvConfs), Password = proplists:get_value(password, EnvConfs), {I, Pa, Pz, Dbdriver, Database, Hostname, Username, Password}. start_db(Dbdriver, Database, Hostname, Username, Password, LogFun) -> erlydb:start(Dbdriver, [{database, Database}, {hostname, Hostname}, {username, Username}, {password, Password}, {logfun, LogFun}]). %% This is developer's entrance to the module. build(AppName) -> io:format("Building development version of ~s.~n", [AppName]), build(AppName, [debug_info], development). build_test(AppName) -> io:format("Building test version of ~s.~n", [AppName]), build(AppName, [debug_info], test). build_product(AppName) -> io:format("Building product version of ~s.~n", [AppName]), build(AppName, [no_debug_info], production). build(AppName, Options, Environment) when is_atom(AppName) -> build(atom_to_list(AppName), Options, Environment); build(AppName, Options, Environment) when is_list(AppName) -> {I, Pa, Pz, Dbdriver, Database, Hostname, Username, Password} = get_conf(Environment), add_code_path(Pa, Pz), start_db(Dbdriver, Database, Hostname, Username, Password, fun erlyweb_app:db_log/4), compile(AppName, Options ++ [{auto_compile, false}], I, Dbdriver). compile(AppName, Options, I, Dbdriver) -> erlyweb:compile("./apps/" ++ AppName, lists:foldl( fun(Dir, Acc) -> [{i, filename:absname(Dir)} | Acc] end, [], I) ++ [{erlydb_driver, Dbdriver}] ++ Options). decompile(AppName, Beam) when is_list(AppName) -> decompile(list_to_atom(AppName), Beam); decompile(AppName, Beam) when is_atom(AppName) -> {BinFilename, SrcFilename} = case AppName of erlyweb -> {"./vendor/erlyweb/ebin/" ++ atom_to_list(Beam), "./erlyweb_" ++ atom_to_list(Beam)}; _ -> {"./apps/" ++ atom_to_list(AppName) ++ "/ebin/" ++ atom_to_list(Beam), "./apps/" ++ atom_to_list(AppName) ++ "_" ++ atom_to_list(Beam)} end, decompile_beam(BinFilename, SrcFilename). decompile_beam(BinFilename, SrcFilename) -> io:format("Beam file: ~s~n", [BinFilename]), io:format("Source file: ~s~n", [SrcFilename++".erl"]), {ok, {_, [{abstract_code, {_, AC}}]}} = beam_lib:chunks(BinFilename, [abstract_code]), %% do not with ".erl" ext?, otherwise will be compiled by erlyweb {ok, S} = file:open(SrcFilename ++ ".erl", write), io:fwrite(S, "~s~n", [erl_prettypr:format(erl_syntax:form_list(AC))]).
To build it,
> erlc -I /opt/local/lib/yaws/include erlyweb_app.erl -o ebin
The erlyweb_app.erl is almost escript ready, but I use it as module functions currently. It's pre-compiled and erlyweb_app.beam is placed under script/ebin
So, I start myapp by steps:
cd \myproject yaws -sname myapp -i --conf config/yaws.conf --erlang "-smp auto" 1> erlyweb_app:build(myapp).
The erlyweb_app.erl is almost escript ready, but I use it as module functions currently. It's pre-compiled and erlyweb_app.beam is placed under script/ebin
After I made changes to myapp, I run above erlyweb_app:build(myapp). again, then everything is up to date.
And if you'd like to build it from another erl shell, try this:
erl -sname erlybird (erlybird@myhost)1> rpc:call(myapp@myhost, erlyweb_app, build, [myapp])
Yes, next version of ErlyBird will support building erlyweb apps remotely in ErlyBird's Erlang shell.
recbird - An Erlang Dynamic Record Inferring Parse Transform
You should have read Yariv's recless blog, a nice blog talking about how to make record accessing simple.
Recless is a static type inferring record parse transform, that means, as described in Yariv's blog:
one main restriction in Recless’s type inference algorithm: function parameters must indicate their record types for type inference to work on them. For instance, this won’t work:get_name(Person) -> Person.name.Instead, you must write this:get_name(Person = #person{}) -> Person.name.
How about a dynamic record inferring solution? I got some idea that was also inspired from my friend Haobo. As I'm familiar with Erlang AST tree when developing ErlyBird, I took a try and got some good result. I named it recbird.
The magic behind recbird is, it parses the Erlang AST tree, and adds some setter/getter functions for each record's field. Then, at runtime, it will detect the first element of record var, and thus knows which setter/getter function should be redirected, and call it.
It just works now, with none limits, you can write R.a.b.c and R.a.b.c = Sth almost every where.
Notice: There may be some bugs.
The perfomance is also reasonable, for example, when you do R.a.b = 'yes' 1,000,000 times, the original Erlang record syntax takes about 300ms in my machine, the recbird is about 310ms. When you run it 10,000,000 times, the recbird is about 150% more time costed than original Erlang record accessing.
The recbird's source code can be got at: recbird.erl
To use it, compile it, include compiled recbird.beam under your code path, add
-compile({parse_transform, recbird}).
in your source file.
A Simple POET State Machine Accepting SAX Events to Build Plain Old Erlang Term
Per previous blogs:
- A Simple XML State Machine Accepting SAX Events to Build xmerl Compitable XML Tree: icalendar demo
- Parse JSON to xmerl Compitable XML Tree via A Simple XML State Machine
I wrote a simple xml state machine that receives SAX events to build xmerl compitable XML tree.
This time, it's a simple POET (Plain Old Erlang Term) state machine, which receives SAX events to build the data in form of List and Tuple.
%%% A state machine which receives sax events and builds a Plain Old Erlang Term -module(poet_sm). -export([state/2]). -export([test/0 ]). -record(poetsmState, { qname = undefined, attributes = [], content = [], parents = [] }). receive_events(Events) -> receive_events(Events, undefined). receive_events([], _States) -> {ok, [], []}; receive_events([Event|T], States) -> case state(Event, States) of {ok, TopObject} -> {ok, TopObject, T}; {error, Reason} -> {error, Reason}; States1 -> receive_events(T, States1) end. state({startDocument}, _StateStack) -> State = #poetsmState{}, [State]; state({endDocument}, StateStack) -> %io:fwrite(user, "endDocument, states: ~p~n", [StateStack]), case StateStack of {ok, TopObject} -> {ok, TopObject}; _ -> {error, io:fwrite( user, "Bad object match, StateStack is: ~n~p~n", [StateStack])} end; state({startElement, _Uri, _LocalName, QName, Attrs}, StateStack) -> %io:fwrite(user, "startElement~n", []), %% pop current State [State|_StatesPrev] = StateStack, #poetsmState{parents=Parents} = State, {_Pos, Attributes1} = lists:foldl( fun ({Key, Value}, {Pos, AccAttrs}) -> Pos1 = Pos + 1, Attr = {atom_to_list(Key), to_poet_value(Value)}, %parents = [{LocalName, Pos1}|Parents]}, {Pos1, [Attr|AccAttrs]} end, {0, []}, Attrs), Parents1 = [{QName, 0}|Parents], %% push new state of Attributes, Content and Parents to StateStack NewState = #poetsmState{qname = QName, attributes = Attributes1, content = [], parents = Parents1}, [NewState|StateStack]; state({endElement, _Uri, _LocalName, QName}, StateStack) -> %% pop current State [State|StatesPrev] = StateStack, #poetsmState{qname=ElemName, attributes=Attributes, content=Content, parents=Parents} = State, %io:fwrite(user, "Element end with Name: ~p~n", [Name]), if QName == undefined -> %% don't care undefined; QName /= ElemName -> throw(lists:flatten(io_lib:format( "Element name match error: ~p should be ~p~n", [QName, ElemName]))); true -> undefined end, %% composite a new object [_|_ParentsPrev] = Parents, Object = if Attributes == [] -> {QName, lists:reverse(Content)}; true -> {QName, lists:reverse(Attributes), lists:reverse(Content)} %parents = ParentsPrev end, %io:fwrite(user, "object: ~p~n", [Object]), %% put Object to parent's content and return new state stack case StatesPrev of [_ParentState|[]] -> %% reached the top now, return final result {ok, Object}; [ParentState|Other] -> #poetsmState{content=ParentContent} = ParentState, ParentContent1 = [Object|ParentContent], %% update parent state and backward to it: ParentState1 = ParentState#poetsmState{content = ParentContent1}, %io:fwrite(user, "endElement, state: ~p~n", [State1]), [ParentState1|Other] end; state({characters, Characters}, StateStack) -> %% pop current State [State|StatesPrev] = StateStack, #poetsmState{qname=_, content=Content, parents=Parents} = State, [{Parent, Pos}|ParentsPrev] = Parents, Pos1 = Pos + 1, Value = to_poet_value(Characters), %parents = [{Parent, Pos1}|ParentsPrev]}, Content1 = [Value|Content], Parents1 = [{Parent, Pos1}|ParentsPrev], UpdatedState = State#poetsmState{content = Content1, parents = Parents1}, [UpdatedState|StatesPrev]. to_poet_value(Name) when is_atom(Name) -> to_poet_value(atom_to_list(Name)); to_poet_value(Chars) when is_list(Chars) -> %% it's string, should convert to binary, since list in poet means array list_to_binary(Chars); to_poet_value(Value) -> Value. test() -> Events = [ {startDocument}, {startElement, "", feed, feed, [{link, "http://lightpole.net"}, {author, "Caoyuan"}]}, {characters, "feed text"}, {startElement, "", entry, entry, [{tag, "Erlang, Function"}]}, {characters, "Entry1's text"}, {endElement, "", entry, entry}, {startElement, "", entry, entry, []}, {characters, "Entry2's text"}, {endElement, "", entry, entry}, {endElement, "", feed, feed}, {endDocument} ], %% Streaming: {ok, Poet1, _Rest} = receive_events(Events), io:fwrite(user, "Streaming Result: ~n~p~n", [Poet1]), {feed,[{"link",<<"http://lightpole.net">>},{"author",<<"Caoyuan">>}], [<<"feed text">>, {entry,[{"tag",<<"Erlang, Function">>}],[<<"Entry1's text">>]}, {entry,[<<"Entry2's text">>]}]} = Poet1.
The result will be something like:
{feed,[{"link",<<"http://lightpole.net">>},{"author",<<"Caoyuan">>}], [<<"feed text">>, {entry,[{"tag",<<"Erlang, Function">>}],[<<"Entry1's text">>]}, {entry,[<<"Entry2's text">>]}]}
The previous iCal and JSON examples can be parsed to POET by modifing the front-end parser a bit.
ErlyBird 0.12.1 released - Erlang IDE based on NetBeans
I'm pleased to announce ErlyBird 0.12.1, an Erlang IDE based on NetBeans.
This is a performance improvement release. This release will only provide all-in-one IDE package, which is in size of 17.3M.
By refining the LL(k) definition of Erlang syntax, I've got ErlyBird parsing performance improved a lot, for example, the time of indexing whole OTP libs is cut to half now.
And this is the first time, ErlyBird works smoothly enough in my compter. I'm with full confidence on Generic Languages Framework of NetBeans now.
Java JRE 5.0+ is requested.
To download, please go to: http://sourceforge.net/project/showfiles.php?group_id=192439
To install:
- Unzip erlybird-bin-0.12.1-ide.zip to somewhere. For Windows user, execute 'bin/erlybird.exe'. For *nix user, 'bin/erlybird'.
- Check/set your OTP path. From [Tools]->[Options], click on 'Miscellanous', then expand 'Erlang Installation', fill in the full path of your 'erl.exe' or 'erl' file. For instance: "C:/erl/bin/erl.exe"
- The default -Xmx option for jvm is set to 256M, if you want to increase it, please open the config file that is located at etc/erlybird.conf, set -J-Xmx of 'default_options'.
When you run ErlyBird first time, the OTP libs will be indexed. The indexing time varies from 10 to 30 minutes deponding on your computer.
Notice:
If you have previous version of ErlyBird installed, please delete the old cache files which are located at:
- *nix: "${HOME}/.erlybird/dev"
- mac os x: "${HOME}/Library/Application Support/erlybird/dev"
- windows: "C:\Documents and Settings\yourusername\.erlybird\dev" or some where
The status of ErlyBird is still Alpha, feedback and bug reports are welcome.
CHANGELOG:
- Performance improvement
ErlyBird 0.12.0 released - Erlang IDE based on NetBeans
I'm pleased to announce ErlyBird 0.12.0, an Erlang IDE based on NetBeans.
This is a bug-fix, performance improvement release. This release will only provide all-in-one IDE package, which is in size of 15.9M.
Java JRE 5.0+ is requested.
To download, please go to: http://sourceforge.net/project/showfiles.php?group_id=192439
To install:
- Unzip erlybird-bin-0.12.0-ide.zip to somewhere. For Windows user, execute 'bin/erlybird.exe'. For *nix user, 'bin/erlybird'.
- Check/set your OTP path. From [Tools]->[Options], click on 'Miscellanous', then expand 'Erlang Installation', fill in the full path of your 'erl.exe' or 'erl' file. For instance: "C:/erl/bin/erl.exe"
- The default -Xmx option for jvm is set to 256M, if you want to increase it, please open the config file that is located at etc/erlybird.conf, set -J-Xmx of 'default_options'.
When you run ErlyBird first time, the OTP libs will be indexed. The indexing time varies from 30 to 60 minutes deponding on your computer.
Notice:
If you have previous version of ErlyBird installed, please delete the old cache files which are located at:
- *nix: "${HOME}/.erlybird/dev"
- mac os x: "${HOME}/Library/Application Support/erlybird/dev"
- windows: "C:\Documents and Settings\yourusername\.erlybird\dev" or some where
The status of ErlyBird is still Alpha, feedback and bug reports are welcome.
CHANGELOG:
- Performance improvement, especially source code rendering performance.
- Highlighting for unbound/unused variables.
- Completion for macros and records.
- Go to source files of -include and -include_lib.
- Erlang shell window in Mac OS X should work now.
- Various bug fixes.
Parse JSON to xmerl Compitable XML Tree via A Simple XML State Machine
Updated Aug 16: Fix bugs when json is an array. Add a 'json:root' element always since valid xml should have a root. Remove 'obj' tag that is not necessary.
Updated Aug 15: A more complete json_parser.erl. Thanks for tonyg's beautiful work, fixed some bugs.
Updated Aug 5: rewrote json_parser.erl base on tonyg's RFC4627 implementation, fixed some bugs.
In my previous blog: A Simple XML State Machine Accepting SAX Events to Build xmerl Compitable XML Tree: icalendar demo, I wrote a simple state machine to parse icalendar to xmerl compitable XML tree. This time, I'll use this state machine to parse a JSON expression to xmerl compitable XML tree, the work is fairly simple:
%%--------------------------------------------------------------------------- %% Copyright (c) 2007 Tony Garnock-Jones%% Copyright (c) 2007 LShift Ltd. %% Copyright (c) 2007 LightPole, Inc. %% %% Permission is hereby granted, free of charge, to any person %% obtaining a copy of this software and associated documentation %% files (the "Software"), to deal in the Software without %% restriction, including without limitation the rights to use, copy, %% modify, merge, publish, distribute, sublicense, and/or sell copies %% of the Software, and to permit persons to whom the Software is %% furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be %% included in all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, %% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF %% MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND %% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS %% BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN %% ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN %% CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE %% SOFTWARE. %%--------------------------------------------------------------------------- %% -module(json_parser). -define(stateMachine, fun xml_sm:state/2). -define(JsonNSUri, "http://www.lightpole.net/xmlns/1.0"). -define(JsonNSAtrr, {'xmlns:json', ?JsonNSUri}). -define(JsonNSRoot, 'json:root'). -define(JsonNSArray, 'json:array'). -record(context, {machine, qname}). -export([parse_to_xml/1, parse_to_poet/1]). -export([test/0]). parse_to_xml(Data) -> parse(Data, #context{machine = fun xml_sm:state/2}). parse_to_poet(Data) -> parse(Data, #context{machine = fun poet_sm:state/2}). parse(Bin, Context) when is_binary(Bin) -> parse(binary_to_list(Bin), Context); parse(Str, #context{machine=MachineFun}=Context) -> State1 = MachineFun({startDocument}, undefined), State2 = parse_root(skip_ws(Str), State1, Context), _State = MachineFun({endDocument}, State2). %% since a valid xml should have a root element, we add one here. parse_root([${|T], State, #context{machine=MachineFun}=Context) -> State1 = MachineFun({startElement, ?JsonNSUri, root, ?JsonNSRoot, [?JsonNSAtrr]}, State), Context1 = Context#context{qname = undefined}, {_Rest, State2} = parse_object(skip_ws(T), State1, Context1), _State = MachineFun({endElement, ?JsonNSUri, root, ?JsonNSRoot}, State2); parse_root([$[|T], State, #context{machine=MachineFun}=Context) -> State1 = MachineFun({startElement, ?JsonNSUri, root, ?JsonNSRoot, [?JsonNSAtrr]}, State), Context1 = Context#context{qname = ?JsonNSArray}, {_Rest, State2} = parse_array(skip_ws(T), State1, Context1), _State = MachineFun({endElement, ?JsonNSUri, root, ?JsonNSRoot}, State2). parse_object([$}|T], State, _Context) -> {T, State}; parse_object([$,|T], State, Context) -> parse_object(skip_ws(T), State, Context); parse_object([$"|T], State, #context{machine=MachineFun}=Context) -> {Rest, ObjNameStr} = parse_string(skip_ws(T), []), ObjName = list_to_atom(ObjNameStr), Context1 = Context#context{qname = ObjName}, [$:|T1] = skip_ws(Rest), {Rest1, State1} = case skip_ws(T1) of [$[|T2] -> %% the value is array, we'll create a list of elements named as this 'ObjName' parse_array(skip_ws(T2), State, Context1); _ -> StateX1 = MachineFun({startElement, "", ObjName, ObjName, []}, State), {RestX, StateX2} = parse_value(skip_ws(T1), StateX1, Context1), StateX3 = MachineFun({endElement, "", ObjName, ObjName}, StateX2), {RestX, StateX3} end, parse_object(skip_ws(Rest1), State1, Context1). parse_array([$]|T], State, _Context) -> {T, State}; parse_array([$,|T], State, Context) -> parse_array(skip_ws(T), State, Context); parse_array(Chars, State, #context{machine=MachineFun, qname=QName}=Context) -> State1 = MachineFun({startElement, "", QName, QName, []}, State), {Rest, State2} = parse_value(Chars, State1, Context), State3 = MachineFun({endElement, "", QName, QName}, State2), parse_array(skip_ws(Rest), State3, Context). parse_value([], State, _Context) -> {[], State}; parse_value("true"++T, State, #context{machine=MachineFun}) -> State1 = MachineFun({characters, "true"}, State), {T, State1}; parse_value("false"++T, State, #context{machine=MachineFun}) -> State1 = MachineFun({characters, "false"}, State), {T, State1}; parse_value("null"++T, State, #context{machine=MachineFun}) -> State1 = MachineFun({characters, "null"}, State), {T, State1}; parse_value([$"|T], State, #context{machine=MachineFun}) -> {Rest, Value} = parse_string(T, []), State1 = MachineFun({characters, Value}, State), {Rest, State1}; parse_value([${|T], State, Context) -> parse_object(skip_ws(T), State, Context); parse_value([$[|T], State, Context) -> parse_array(skip_ws(T), State, Context); parse_value(Chars, State, #context{machine=MachineFun}) -> {Rest, Value} = parse_number(skip_ws(Chars), []), State1 = MachineFun({characters, Value}, State), {Rest, State1}. parse_string([$"|T], Acc) -> {T, lists:reverse(Acc)}; parse_string([$\\, Key|T], Acc) -> parse_escaped_char(Key, T, Acc); parse_string([H|T], Acc) -> parse_string(T, [H|Acc]). parse_escaped_char($b, Rest, Acc) -> parse_string(Rest, [8|Acc]); parse_escaped_char($t, Rest, Acc) -> parse_string(Rest, [9|Acc]); parse_escaped_char($n, Rest, Acc) -> parse_string(Rest, [10|Acc]); parse_escaped_char($f, Rest, Acc) -> parse_string(Rest, [12|Acc]); parse_escaped_char($r, Rest, Acc) -> parse_string(Rest, [13|Acc]); parse_escaped_char($/, Rest, Acc) -> parse_string(Rest, [$/|Acc]); parse_escaped_char($\\, Rest, Acc) -> parse_string(Rest, [$\\|Acc]); parse_escaped_char($", Rest, Acc) -> parse_string(Rest, [$"|Acc]); parse_escaped_char($u, [D0, D1, D2, D3|Rest], Acc) -> parse_string(Rest, [(digit_hex(D0) bsl 12) + (digit_hex(D1) bsl 8) + (digit_hex(D2) bsl 4) + (digit_hex(D3))|Acc]). digit_hex($0) -> 0; digit_hex($1) -> 1; digit_hex($2) -> 2; digit_hex($3) -> 3; digit_hex($4) -> 4; digit_hex($5) -> 5; digit_hex($6) -> 6; digit_hex($7) -> 7; digit_hex($8) -> 8; digit_hex($9) -> 9; digit_hex($A) -> 10; digit_hex($B) -> 11; digit_hex($C) -> 12; digit_hex($D) -> 13; digit_hex($E) -> 14; digit_hex($F) -> 15; digit_hex($a) -> 10; digit_hex($b) -> 11; digit_hex($c) -> 12; digit_hex($d) -> 13; digit_hex($e) -> 14; digit_hex($f) -> 15. finish_number(Rest, Acc) -> Value = lists:reverse(Acc), % Value = % case catch list_to_integer(Str) of % {'EXIT', _} -> list_to_float(Str); % Number -> Number % end, {Rest, Value}. parse_number([], _Acc) -> exit(syntax_error); parse_number([$-|T], Acc) -> parse_number1(T, [$-|Acc]); parse_number(Rest, Acc) -> parse_number1(Rest, Acc). parse_number1(Rest, Acc) -> {Acc1, Rest1} = parse_int_part(Rest, Acc), case Rest1 of [] -> finish_number([], Acc1); [$.|More] -> {Acc2, Rest2} = parse_int_part(More, [$.| Acc1]), parse_exp(Rest2, Acc2, false); _ -> parse_exp(Rest1, Acc1, true) end. parse_int_part([], Acc) -> {Acc, []}; parse_int_part([Ch|Rest], Acc) -> case is_digit(Ch) of true -> parse_int_part(Rest, [Ch | Acc]); false -> {Acc, [Ch | Rest]} end. parse_exp([$e|T], Acc, NeedFrac) -> parse_exp1(T, Acc, NeedFrac); parse_exp([$E|T], Acc, NeedFrac) -> parse_exp1(T, Acc, NeedFrac); parse_exp(Rest, Acc, _NeedFrac) -> finish_number(Rest, Acc). parse_exp1(Rest, Acc, NeedFrac) -> {Acc1, Rest1} = parse_signed_int_part(Rest, if NeedFrac -> [$e, $0, $.|Acc]; true -> [$e|Acc] end), finish_number(Rest1, Acc1). parse_signed_int_part([$+|T], Acc) -> parse_int_part(T, [$+|Acc]); parse_signed_int_part([$-|T], Acc) -> parse_int_part(T, [$-|Acc]); parse_signed_int_part(Rest, Acc) -> parse_int_part(Rest, Acc). is_digit(C) when is_integer(C) andalso C >= $0 andalso C =< $9 -> true; is_digit(_) -> false. skip_ws([H|T]) when H =< 32 -> skip_ws(T); skip_ws(Chars) -> Chars. test() -> Text1 = "{\"firstname\":\"Caoyuan\", \"iq\":\"150\"}", {ok, Xml1} = parse_to_xml(Text1), XmlText1 = lists:flatten(xmerl:export_simple([Xml1], xmerl_xml)), io:fwrite(user, "Parsed XML: ~n~p~n", [XmlText1]), {ok, Poet1} = parse_to_poet(Text1), io:fwrite(user, "Parsed POET: ~n~p~n", [Poet1]), Text2 = "[{\"firstname\":\"Caoyuan\", \"iq\":\"150\"}, {\"firstname\":\"Haobo\", \"iq\":150}]", {ok, Xml2} = parse_to_xml(Text2), XmlText2 = lists:flatten(xmerl:export_simple([Xml2], xmerl_xml)), io:fwrite(user, "Parsed: ~n~p~n", [XmlText2]), Text = " {\"businesses\": [{\"address1\": \"650 Mission Street\", \"address2\": \"\", \"avg_rating\": 4.5, \"categories\": [{\"category_filter\": \"localflavor\", \"name\": \"Local Flavor\", \"search_url\": \"http://lightpole.net/search\"}], \"city\": \"San Francisco\", \"distance\": 0.085253790020942688, \"id\": \"4kMBvIEWPxWkWKFN__8SxQ\", \"latitude\": 37.787185668945298, \"longitude\": -122.40093994140599}, {\"address1\": \"25 Maiden Lane\", \"address2\": \"\", \"avg_rating\": 5.0, \"categories\": [{\"category_filter\": \"localflavor\", \"name\": \"Local Flavor\", \"search_url\": \"http://lightpole.net/search\"}], \"city\": \"San Francisco\", \"distance\": 0.23186808824539185, \"id\": \"O1zPF_b7RyEY_NNsizX7Yw\", \"latitude\": 37.788387, \"longitude\": -122.40401}]} ", {ok, Xml} = parse_to_xml(Text), %io:fwrite(user, "Xml Tree: ~p~n", [Xml]), XmlText = lists:flatten(xmerl:export_simple([Xml], xmerl_xml)), io:fwrite(user, "Parsed: ~n~p~n", [XmlText]), Latitude1 = xmerl_xpath:string("/lp:root/businesses[1]/latitude/text()", Xml), io:format(user, "Latitude1: ~p~n", [Latitude1]).
The result will be something like:
<?xml version="1.0"?> <json:root xmlns:json="http://www.lightpole.net/xmlns/1.0"> <businesses> <address1>650 Mission Street</address1> <address2></address2> <avg_rating>4.5</avg_rating> <categories> <category_filter>localflavor</category_filter> <name>Local Flavor</name> <search_url>http://lightpole.net/search</search_url> </categories> <city>San Francisco</city> <distance>0.085253790020942688</distance> <id>4kMBvIEWPxWkWKFN__8SxQ</id> <latitude>37.787185668945298</latitude> <longitude>-122.40093994140599</longitude> </businesses> <businesses> <address1>25 Maiden Lane</address1> <address2></address2> <avg_rating>5.0</avg_rating> <categories> <category_filter>localflavor</category_filter> <name>Local Flavor</name> <search_url>http://lightpole.net/search</search_url> </categories> <city>San Francisco</city> <distance>0.23186808824539185</distance> <id>O1zPF_b7RyEY_NNsizX7Yw</id> <latitude>37.788387</latitude> <longitude>-122.40401</longitude> </businesses> </root>
Now you fecth element by:
> [Latitude1] = xmerl_xpath:string("/json:root/businesses[1]/latitude/text()", Xml), > Latitude1#xmlText.value. "37.787185668945298"
Next time, I'll write a simple Erlang Data state machine, which will parse icalendar and json to simple Erlang Lists + Tuples.
The code of xml_sm.erl can be found in my previous blog.
A Simple XML State Machine Accepting SAX Events to Build xmerl Compitable XML Tree: icalendar demo
xmerl is a full XML functionality in Erlang, with a lot of features like XPATH, XSLT, event_function, acc_function etc. Well, now I just want to get icalendar to be parsed to form of xmerl tree, which will contain #xmlElement, #xmlAttribute, #xmlText etc, and easily to apply XPATH on it.
How about an approach that the parser just generates SAX events, and then, by attaching to a callback state machine to build a JSON or XML tree, or anything else?
I hoped xmerl is something like this, i.e. a parser to generate SAX events, and a state machine to accept the events and build the XML tree. I digged into xmerl's code, but, unfortunately, the parser and state machine are coupled together.
So I wrote a simple state machine which just receives SAX events to build a xmerl compitable XML tree. And, I applied it to icalendar.
I like this idea, by using SAX events as the common interface, I only need to write a another JSON state machine later, then, the result will be JSON of icalendar. I can share the same parser which just generates SAX events.
Here's the code, which is not completed yet, just to show how a SAX interface can serve a lot.
%%% A state machine which receives sax events and builds a xmerl compitable tree -module(xml_sm). -include_lib("xmerl/include/xmerl.hrl"). -export([state/2]). -export([test/0 ]). -record(xmlsmState, { qname = undefined, attributes = [], content = [], parents = [] }). receive_events(Events) -> receive_events(Events, undefined). receive_events([], _States) -> {ok, [], []}; receive_events([Event|T], States) -> case state(Event, States) of {ok, TopElement} -> {ok, TopElement, T}; {error, Reason} -> {error, Reason}; States1 -> receive_events(T, States1) end. state({startDocument}, _StateStack) -> State = #xmlsmState{}, [State]; state({endDocument}, StateStack) -> %io:fwrite(user, "endDocument, states: ~p~n", [StateStack]), case StateStack of {ok, TopElement} -> {ok, TopElement}; _ -> {error, io:fwrite(user, "Bad element match, StateStack is: ~n~p~n", [StateStack])} end; state({startElement, _Uri, _LocalName, QName, Attrs}, StateStack) -> %io:fwrite(user, "startElement~n", []), %% pop current State [State|_StatesPrev] = StateStack, #xmlsmState{parents=Parents} = State, {_Pos, Attributes1} = lists:foldl( fun ({Key, Value}, {Pos, AccAttrs}) -> Pos1 = Pos + 1, Attr = #xmlAttribute{name = Key, value = Value, parents = [{QName, Pos1}|Parents]}, {Pos1, [Attr|AccAttrs]} end, {0, []}, Attrs), Parents1 = [{QName, 0}|Parents], %% push new state of Attributes, Content and Parents to StateStack NewState = #xmlsmState{qname = QName, attributes = Attributes1, content = [], parents = Parents1}, [NewState|StateStack]; state({endElement, _Uri, _LocalName, QName}, StateStack) -> %% pop current State [State|StatesPrev] = StateStack, #xmlsmState{qname=ElemName, attributes=Attributes, content=Content, parents=Parents} = State, %io:fwrite(user, "Element end with Name: ~p~n", [Name]), if QName == undefined -> %% don't care undefined; QName /= ElemName -> throw(lists:flatten(io_lib:format( "Element name match error: ~p should be ~p~n", [QName, ElemName]))); true -> undefined end, %% composite a new element [_|ParentsPrev] = Parents, Element = #xmlElement{name = QName, attributes = lists:reverse(Attributes), content = lists:reverse(Content), parents = ParentsPrev}, %io:fwrite(user, "Element: ~p~n", [Element]), %% put Element to parent's content and return new state stack case StatesPrev of [_ParentState|[]] -> %% reached the top now, return final result {ok, Element}; [ParentState|Other] -> #xmlsmState{content=ParentContent} = ParentState, ParentContent1 = [Element|ParentContent], %% update parent state and backward to it: ParentState1 = ParentState#xmlsmState{content = ParentContent1}, %io:fwrite(user, "endElement, state: ~p~n", [State1]), [ParentState1|Other] end; state({characters, Characters}, StateStack) -> %% pop current State [State|StatesPrev] = StateStack, #xmlsmState{content=Content, parents=Parents} = State, [{Parent, Pos}|ParentsPrev] = Parents, Pos1 = Pos + 1, Text = #xmlText{value = Characters, parents = [{Parent, Pos1}|ParentsPrev]}, Content1 = [Text|Content], Parents1 = [{Parent, Pos1}|ParentsPrev], UpdatedState = State#xmlsmState{content = Content1, parents = Parents1}, [UpdatedState|StatesPrev]. test() -> Events = [ {startDocument}, {startElement, "", feed, feed, [{link, "http://lightpole.net"}, {author, "Caoyuan"}]}, {characters, "feed text"}, {startElement, "", entry, entry, [{tag, "Erlang, Function"}]}, {characters, "Entry1's text"}, {endElement, "", entry, entry}, {startElement, "", entry, entry, []}, {characters, "Entry2's text"}, {endElement, "", entry, entry}, {endElement, "", feed, feed}, {endDocument} ], %% Streaming: {ok, Xml1, _Rest} = receive_events(Events), io:fwrite(user, "Streaming Result: ~n~p~n", [Xml1]), %% Stepped: FunCallback = fun xml_sm:state/2, FinalStates = lists:foldl( fun (Event, States) -> FunCallback(Event, States) end, undefined, Events), {ok, Xml2} = FinalStates, XmlText = lists:flatten(xmerl:export_simple([Xml2], xmerl_xml)), io:fwrite(user, "Stepped Result: ~n~p~n", [XmlText]).
And the primary icalendar front end:
-module(ical_parser). -include_lib("xmerl/include/xmerl.hrl"). -export([parse/1 ]). -export([test/0 ]). -define(stateMachine, fun xml_sm:state/2). parse(Text) -> States1 = ?stateMachine({startDocument}, undefined), States2 = parse_line(skip_ws(Text), 0, States1), ?stateMachine({endDocument}, States2). parse_line([], _Line, States) -> States; parse_line([$\s|T], Line, States) -> parse_line(T, Line, States); parse_line([$\t|T], Line, States) -> parse_line(T, Line, States); parse_line([$\r|T], Line, States) -> parse_line(T, Line, States); parse_line([$\n|T], Line, States) -> parse_line(T, Line + 1, States); parse_line("BEGIN"++T, Line, States) -> case skip_ws(T) of [$:|T1] -> {Rest, Line1, Name} = parse_component_name(skip_ws(T1), Line, States, []), %io:fwrite(user, "Component started: ~p~n", [Name]), States1 = ?stateMachine({startElement, "", Name, Name, []}, States), parse_line(skip_ws(Rest), Line1, States1); _ -> error end; parse_line("END"++T, Line, States) -> case skip_ws(T) of [$:|T1] -> {Rest, Line1, Name} = parse_component_name(skip_ws(T1), Line, States, []), States1 = ?stateMachine({endElement, "", Name, Name}, States), parse_line(skip_ws(Rest), Line1, States1); _ -> error end; parse_line(Text, Line, States) -> {Rest, Line1, {Name, Params}, Value} = parse_prop(skip_ws(Text), Line, States, {[], []}), States1 = ?stateMachine({startElement, "", Name, Name, Params}, States), States2 = ?stateMachine({characters, Value}, States1), States3 = ?stateMachine({endElement, "", Name, Name}, States2), parse_line(skip_ws(Rest), Line1, States3). parse_component_name([$\r|T], Line, States, Name) -> parse_component_name(T, Line, States, Name); parse_component_name([$\n|T], Line, States, Name) -> case unfolding_line(T) of {true, Rest} -> parse_component_name(Rest, Line, States, Name); {false, Rest} -> {Rest, Line + 1, list_to_atom(string:to_lower(lists:reverse(Name)))} end; parse_component_name([H|T], Line, States, Name) -> parse_component_name(skip_ws(T), Line, States, [H|Name]). parse_prop([$:|T], Line, States, {Name, NameParams}) -> PropName = list_to_atom(string:to_lower(lists:reverse(Name))), PropNameParams = lists:reverse(NameParams), %io:fwrite(user, "parsed prop name: ~p, with params: ~p~n", [PropName, NameParams]), {Rest, Line1, Value} = parse_prop_value(T, Line, States, []), %io:fwrite(user, "parsed prop : ~p~n", [{PropName, NameParams, Value}]), {Rest, Line1, {PropName, PropNameParams}, Value}; parse_prop([$;|T], Line, States, {Name, NameParams}) -> {Rest, Line1, ParamName, ParamValue} = parse_param(T, Line, States, []), parse_prop(Rest, Line1, States, {Name, [{ParamName, ParamValue}|NameParams]}); parse_prop([H|T], Line, States, {Name, NameParams}) -> parse_prop(skip_ws(T), Line, States, {[H|Name], NameParams}). parse_prop_value([$\r|T], Line, States, Value) -> parse_prop_value(T, Line, States, Value); parse_prop_value([$\n|T], Line, States, Value) -> case unfolding_line(T) of {true, Rest} -> parse_prop_value(Rest, Line, States, Value); {false, Rest} -> {Rest, Line + 1, lists:reverse(Value)} end; parse_prop_value([H|T], Line, States, Value) -> parse_prop_value(T, Line, States, [H|Value]). parse_param([$=|T], Line, States, Name) -> ParamName = list_to_atom(string:to_lower(lists:reverse(Name))), {Rest, Line1, Value} = parse_param_value(T, Line, States, []), {Rest, Line1, ParamName, Value}; parse_param([H|T], Line, States, Name) -> parse_param(skip_ws(T), Line, States, [H|Name]). parse_param_value([$;|T], Line, _States, Value) -> {T, Line, lists:reverse(Value)}; parse_param_value([$:|T], Line, _States, Value) -> %% keep $: for end of prop name {[$:|T], Line, lists:reverse(Value)}; parse_param_value([H|T], Line, States, Value) -> parse_param_value(T, Line, States, [H|Value]). unfolding_line([$\s|T]) -> {true, T}; %% space unfolding_line([$\t|T]) -> {true, T}; %% htab unfolding_line(Chars) -> {false, Chars}. skip_ws([$\s|T]) -> skip_ws(T); skip_ws([$\t|T]) -> skip_ws(T); skip_ws(Text) -> Text. test() -> Text = " BEGIN:VCALENDAR METHOD:PUBLISH X-WR-CALNAME:Mi's Calendar VERSION:2.0 PRODID:Spongecell CALSCALE:GREGORIAN BEGIN:VEVENT DTSTART;TZID=America/Los_Angeles:20061206T120000 DTSTAMP:20070728T004842 LOCATION:Gordon Biersch, 640 Emerson St, Palo Alto, CA URL: UID:295803:spongecell.com SUMMARY:All hands meeting RRULE:FREQ=WEEKLY;INTERVAL=1 DTEND;TZID=America/Los_Angeles:20061206T130000 DESCRIPTION: END:VEVENT BEGIN:VEVENT DTSTART;TZID=America/Los_Angeles:20061207T120000 DTSTAMP:20070728T004842 LOCATION:395 ano nuevo ave\, sunnyvale\, ca URL: UID:295802:spongecell.com SUMMARY:Company lunch RRULE:FREQ=WEEKLY;INTERVAL=1 DTEND;TZID=America/Los_Angeles:20061207T130000 DESCRIPTION:Let's have lots of beer!! (well\, and some code review :) END:VEVENT BEGIN:VEVENT DTSTART;TZID=America/Los_Angeles:20061213T123000 DTSTAMP:20070728T004842 LOCATION:369 S California Ave\, Palo Alto\, CA URL: UID:295714:spongecell.com SUMMARY:Ben is back.. want to meet again DTEND;TZID=America/Los_Angeles:20061213T133000 DESCRIPTION:Re: Ben is back.. want to meet again\n Marc END:VEVENT BEGIN:VEVENT DTSTART;TZID=America/Los_Angeles:20070110T200000 DTSTAMP:20070728T004842 LOCATION: URL: UID:304529:spongecell.com SUMMARY:flight back home DTEND;TZID=America/Los_Angeles:20070110T210000 DESCRIPTION: END:VEVENT BEGIN:VTIMEZONE TZID:America/Los_Angeles BEGIN:STANDARD DTSTART:20071104T000000 TZNAME:PST RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU TZOFFSETFROM:-0700 TZOFFSETTO:-0800 END:STANDARD BEGIN:DAYLIGHT DTSTART:20070311T000000 TZNAME:PDT RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=1SU TZOFFSETFROM:-0800 TZOFFSETTO:-0700 END:DAYLIGHT END:VTIMEZONE END:VCALENDAR ", io:fwrite(user, "Text: ~s~n", [Text]), {ok, Xml} = parse(Text), XmlText = lists:flatten(xmerl:export_simple([Xml], xmerl_xml)), io:fwrite(user, "Parsed: ~n~p~n", [XmlText]).
You may have noticed, the ?stateMachine can be pointed to a json_machine:state/2 some day, and we can get a JSON result without modification of icalendar.erl.
This also can be applied on JSON<->XML transform. Actually, I think SAX events is a good interface for various formats transform of data object. It's also a bit Erlang Style (Event passing). The parser/state-machine can communicate via SAX events as two separate processes and live with send/receive.
From Rails to Erlyweb - Part IV
IV. Support Mysql Spatial Extensions via erlydb_mysql
ErlyDB supports most database operations, and it can also be extended to support more complex operations. Here is an example, where I need to support Mysql spatial extensions.
Assume we have a table: places, which has a mysql point field: location, here is the code:
-module(places). -export([get_location_as_tuple/1, set_location_by_tuple/2 ]). %% @spec get_location_as_tuple(Id::integer()) -> {float(), float()} get_location_as_tuple(Id) -> ESQL = {esql, {select, 'AsText(location)', {from, ?MODULE}, {where, {'id', '=', Id}}}}, case erlydb_mysql:q(ESQL) of %% Example: %% {data, {mysql_result, [{<<>>, <<"AsText(location)">>, 8192, 'VAR_STRING'}], %% [[<<"POINT(-122.292 37.8341)">>]], %% 0, %% []}} {data, {mysql_result, [{_, <<"AsText(location)">>, _, _}], [[FirstResult]|_Rest], _, _}} -> case io_lib:fread("POINT(~f ~f)", binary_to_list(FirstResult)) of {ok, [X, Y], _} -> {X, Y}; _Else -> undefined end; _Error -> undefined end. %% @spec set_location_by_tuple(Id::integer(), {X::float(), Y::float()}) -> ok | error set_location_by_tuple(Id, {X, Y}) -> %% "UPDATE places SET location = PointFromText(\'POINT(1 1)\') WHERE (id = 1)" PointFromText = point_tuple_to_point_from_text({X, Y}), ESQL = {esql, {update, ?MODULE, [{location, PointFromText}], {'id', '=', Id}}}, Options = [{allow_unsafe_statements, true}], case erlydb_mysql:q(ESQL, Options) of {updated, {mysql_result, [], [], _UpdatedNum, []}} -> ok; _Error -> error end. point_tuple_to_point_from_text({X, Y}) -> %% as mysql support float in format of someting like 1.20002300000000005298e+02, %% we can just apply float_to_list/1 to X and Y PointFromText = lists:flatten(io_lib:fwrite("PointFromText('POINT(~f ~f)')", [X, Y])), list_to_atom(PointFromText). %% 'PointFromText(\'POINT(X Y)\')'
Now we can:
> places:set_location_by_tuple(6, {-11.11, 88.88}). > places:get_location_as_tuple(6). {-11.11, 88.88}
AIOTrade Is Open for Submissions
Here's the announcement from Moshe at the AIOTrade project page on sourceforce.net:
Lately I was added by as a developer in the project and I created a branch called "opencommunity". This branch should contain a development version that is built by contribution of the community in order to answer better the needs of a trader.
Any contribution, any code, designs, ideas, bug fixes, etc., should be send to me and I'll integrate it (the contributers will be credited, of course)
I can be contacted through the forum of the AIOTrade project or directly through aiotrade -d0t- submit -at- gmail -d0t- com or by using the secure client at https://sourceforge.net/sendmessage.php?touser=930864
Moshe
Good works! Moshe
HTML Entity Refs and xmerl
According to [erlang-bugs] xmerl and standard HTML entity refs, currently xmerl_scan only recognizes the very limited set of entity references. In brief, if you try to xmerl:scan xml text that includes standard HTML entity refs, such as nbsp, iexcl, pound, frac14, etc. you'll encounter something like:
16> edoc:file("exprecs.erl"). 2670- fatal: {unknown_entity_ref,nbsp} 2580- fatal: error_scanning_entity_ref exprecs.erl, in module header: at line 28: error in XML parser: {fatal, {error_scanning_entity_ref, {file,file_name_unknown}, {line,86}, {col,18}}}. ** exited: error **
Ulf Wiger said:
... I realize that xmerl can be customized with a rules function which, for example, can handle entity references...
So I take a try by writing a piece of code (html_entity_refs.erl) to parse a HTML entity ref DTD file to ets rules, then:
xmerl_scan:string(XmlText, [{rules, html_entity_refs:get_xmerl_rules()}]).
Yes, this works. But for a 3MB testing file, the parsing took about 30 seconds.
How about convert these entity refs to utf-8 chars first, then apply xmerl_scan to it?
I wrote another piece of code, and now:
xmerl_scan:string(html_entity_refs:decode_for_xml(XmlText)).
This time, the decoding+parsing time is about 5 seconds, it's 6 times faster than ets rules solution.
The html_entity_refs.erl can be got from:
http://caoyuan.googlecode.com/svn/trunk/erlang/html_entity_refs.erl
AIOTrade Goes to BSD License Again
Updated June 21: I've updated the source code in trunk to BSD License.
I decide to re-open AIOTrade, an open-source, free stock technical analysis and charting platform based on NetBeans to BSD license. And welcome the new developer Moshe who will maintain a community branch.
As I'm working hard for my friends project, I have no much time to go with this project in the near future, so, more community members will be helpful for this project.
I'll update the source tree in trunk to BSD license in one week.
I have now a brand new Macbook in white, with 2G memory and Parallels+Windows XP installed, so I can make sure AIOTrade and ErlyBird being Mac OS friendly.
Vi Module Meets ErlyBird
There have been several Erlang development tools: Erlang module for Emacs, for vim, and Erlide for Eclipse. Why I write another Erlang IDE based on NetBeans?
Erlang for Emacs runs smoothly on my computer, but the distel module can not communicate with Erlang node on my Windows XP, that means I can not have the auto-completion, go to declaration features working; Erlang for vim is not a complete IDE yet; Erlide hangs on my Windows XP too. So I write ErlyBird.
But I'm actually a vi fun, so I just download and install the excellent jVi module to ErlyBird, which is a fully functional vi environment with good performance. There is an article talking about vi integration with NetBeans, which can also be applied to ErlyBird.
I'm now with fun with Vi on ErlyBird on my daily job.
The biggest issue for ErlyBird currently is the rendering performance, which causes performance slowing down if you run ErlyBird a while. I'm not sure if this issue depends on Generic Language Framework module of NetBeans. After I get the new laptop which with 2G memory next week, I may do some profile analysis.
I've also written some code to talk with Erlang Node from ErlyBird, everything looks smooth too.
I'll fly to San Francisco next week, to meet my new and old friends there.
It seems that this has been a world you should mix Vi/Netbeans, Java/Erlang, Beijing/Vancouver/San Francisco, whatever, together? A dynamical, colorful, multi-culture world, you have to look for the truths carefully, continually.
ErlyBird 0.11.2 released - An Erlang IDE based on NetBeans
I'm pleased to announce ErlyBird 0.11.2, an Erlang IDE based on NetBeans.
This is a bug-fix, stabilization release. Since I tightly modified GSF/GLF modules of NetBeans, this release will only provide all-in-one IDE package, which is in size of 14.8M.
To download, please go to: http://sourceforge.net/project/showfiles.php?group_id=192439
To install:
- Unzip erlybird-bin-0.11.2.zip to somewhere. For Windows user, executee 'bin/erlybird.exe'. For *nix user, 'bin/erlybird'.
- Check/set your OTP path. From [Tools]->[Options], click on 'Miscellanous', then expand 'Erlang Installation', fill in the full path of your 'erl.exe' or 'erl' file. For instance: "C:/erl/bin/erl.exe"
- The default -Xmx option for jvm is set to 256M, if you want to increase, open the config file of ErlyBird that is located at etc/erlybird.conf, set -J-Xmx in line of 'default_options'
The status of ErlyBird is still Alpha, feedback and bug reports are welcome.
CHANGELOG:
- Indexing will skip too big files according to the max memeory. This avoids ErlyBird to hang when indexing.
- If erl/erl.exe is under the environment path, ErlyBird will try to set Erlang Installation path automatically.
- Including function args in completion suggestion.
- Various bugs fixes especially for stabilization.
From Rails to Erlyweb - Part III
3. The Magic Behind Erlyweb
With dynamic typing, hot code swapping, built-in parsing and compilation tools, Erlang is also suitable for dynamic meta-programming. Erlyweb uses a small convenient tool smerl to generate db record CRUD code automatically.
The music example on erlyweb.org shows the magic:
Define a simple musician.erl module (just one line here):
-module(musician).
Then, you will get a long list functions for musician module after erlyweb:compile("apps/music"). Sounds similar to Rails.
I like to watch magic show, but I can't stand that I do not know the things behind magic in programming. For Rails, the magic behind it always makes me headache, it's too difficult to follow who, where, some code are injected into a class or module. But in Erlang, it's super easy.
I add a simple function to erlyweb_app.erl (please see my previous post):
-export([decompile/2]). decompile(AppName, Beam) when is_atom(AppName) -> decompile(atom_to_list(AppName), Beam); decompile(AppName, Beam) when is_list(AppName) -> BinFilename = "./apps/" ++ AppName ++ "/ebin/" ++ atom_to_list(Beam), io:format("Beam file: ~s~n", [BinFilename]), {ok, {_, [{abstract_code, {_, AC}}]}} = beam_lib:chunks(BinFilename, [abstract_code]), SrcFilename = "./apps/" ++ AppName ++ "_" ++ atom_to_list(Beam), {ok, S} = file:open(SrcFilename ++ ".erl", write), io:fwrite(S, "~s~n", [erl_prettypr:format(erl_syntax:form_list(AC))]).
Now type erlyweb_decompile(music, musician) under the Erlang shell, I get a file: music_musician.erl under folder myproject/apps/ (I put these decompiled source files under /myproject/apps/ to avoid that they are auto-compiled to beams by erlyweb again ):
-file("musician", 1). -module(musician). -export([relations/0, fields/0, table/0, type_field/0, db_table/0, db_field/1, is_new/1, is_new/2, get_module/1, to_iolist/1, to_iolist/2, field_to_iolist/2, new/0, new_with/1, new_with/2, new_from_strings/1, set_fields/2, set_fields/3, set_fields_from_strs/2, field_from_string/2, save/1, insert/1, update/1, update/2, delete/1, delete_id/1, delete_where/1, delete_all/0, transaction/1, before_save/1, after_save/1, before_delete/1, after_delete/1, after_fetch/1, find/2, find_first/2, find_max/3, find_range/4, find_id/1, aggregate/4, count/0, get/2, set_related_one_to_many/2, find_related_one_to_many/2, find_related_many_to_one/4, aggregate_related_many_to_one/6, add_related_many_to_many/3, remove_related_many_to_many/3, remove_related_many_to_many_all/5, find_related_many_to_many/5, aggregate_related_many_to_many/7, find_related_many_first/4, find_related_many_max/5, find_related_many_range/6, aggregate_related_many/6, do_save/1, do_delete/1, field_names_for_query/0, field_names_for_query/1, field_to_iolist/1, set/3, db_pk_fields/0, get_pk_fk_fields/0, get_pk_fk_fields2/0, db_fields/0, db_field_names/0, db_field_names_str/0, db_field_names_bin/0, db_num_fields/0, id/1, id/2, name/1, name/2, birth_date/1, birth_date/2, instrument/1, instrument/2, bio/1, bio/2, new/4, driver/0, count/3, count/1, count/2, count_with/2, avg/3, avg/1, avg/2, avg_with/2, min/3, min/1, min/2, min_with/2, max/3, max/1, max/2, max_with/2, sum/3, sum/1, sum/2, sum_with/2, stddev/3, stddev/1, stddev/2, stddev_with/2, find/0, find/1, find_with/1, find_first/0, find_first/1, find_first_with/1, find_max/1, find_max/2, find_max_with/2, find_range/2, find_range/3, find_range_with/3]). relations() -> erlydb_base:relations(). fields() -> erlydb_base:fields(). table() -> erlydb_base:table(). type_field() -> erlydb_base:type_field(). db_table() -> erlydb_base:db_table(musician). db_field(FieldName) -> erlydb_base:db_field(musician, FieldName). is_new(Rec) -> erlydb_base:is_new(Rec). is_new(Rec, Val) -> erlydb_base:is_new(Rec, Val). get_module(Rec) -> erlydb_base:get_module(Rec). to_iolist(Recs) -> erlydb_base:to_iolist(musician, Recs). to_iolist(Recs, ToIolistFun) -> erlydb_base:to_iolist(musician, Recs, ToIolistFun). field_to_iolist(Val, _Field) -> erlydb_base:field_to_iolist(Val, _Field). new() -> erlydb_base:new(musician). new_with(Fields) -> erlydb_base:new_with(musician, Fields). new_with(Fields, ToFieldFun) -> erlydb_base:new_with(musician, Fields, ToFieldFun). new_from_strings(Fields) -> erlydb_base:new_from_strings(musician, Fields). set_fields(Record, Fields) -> erlydb_base:set_fields(musician, Record, Fields). set_fields(Record, Fields, ToFieldFun) -> erlydb_base:set_fields(musician, Record, Fields, ToFieldFun). set_fields_from_strs(Record, Fields) -> erlydb_base:set_fields_from_strs(musician, Record, Fields). field_from_string(ErlyDbField, undefined) -> erlydb_base:field_from_string(ErlyDbField, undefined). save(Rec) -> erlydb_base:save(Rec). insert(Recs) -> erlydb_base:insert(Recs). update(Props) -> erlydb_base:update(musician, Props). update(Props, Where) -> erlydb_base:update(musician, Props, Where). delete(Rec) -> erlydb_base:delete(Rec). delete_id(Id) -> erlydb_base:delete_id(musician, Id). delete_where(Where) -> erlydb_base:delete_where(musician, Where). delete_all() -> erlydb_base:delete_all(musician). transaction(Fun) -> erlydb_base:transaction(musician, Fun). before_save(Rec) -> erlydb_base:before_save(Rec). after_save(Rec) -> erlydb_base:after_save(Rec). before_delete(Rec) -> erlydb_base:before_delete(Rec). after_delete({_Rec, Num}) -> erlydb_base:after_delete({_Rec, Num}). after_fetch(Rec) -> erlydb_base:after_fetch(Rec). find(Where, Extras) -> erlydb_base:find(musician, Where, Extras). find_first(Where, Extras) -> erlydb_base:find_first(musician, Where, Extras). find_max(Max, Where, Extras) -> erlydb_base:find_max(musician, Max, Where, Extras). find_range(First, Max, Where, Extras) -> erlydb_base:find_range(musician, First, Max, Where, Extras). find_id(Id) -> erlydb_base:find_id(musician, Id). aggregate(AggFunc, Field, Where, Extras) -> erlydb_base:aggregate(musician, AggFunc, Field, Where, Extras). count() -> erlydb_base:count(musician). get(Idx, Rec) -> erlydb_base:get(Idx, Rec). set_related_one_to_many(Rec, Other) -> erlydb_base:set_related_one_to_many(Rec, Other). find_related_one_to_many(OtherModule, Rec) -> erlydb_base:find_related_one_to_many(OtherModule, Rec). find_related_many_to_one(OtherModule, Rec, Where, Extras) -> erlydb_base:find_related_many_to_one(OtherModule, Rec, Where, Extras). aggregate_related_many_to_one(OtherModule, AggFunc, Rec, Field, Where, Extras) -> erlydb_base:aggregate_related_many_to_one(OtherModule, AggFunc, Rec, Field, Where, Extras). add_related_many_to_many(JoinTable, Rec, OtherRec) -> erlydb_base:add_related_many_to_many(JoinTable, Rec, OtherRec). remove_related_many_to_many(JoinTable, Rec, OtherRec) -> erlydb_base:remove_related_many_to_many(JoinTable, Rec, OtherRec). remove_related_many_to_many_all(JoinTable, OtherTable, Rec, Where, Extras) -> erlydb_base:remove_related_many_to_many_all(JoinTable, OtherTable, Rec, Where, Extras). find_related_many_to_many(OtherModule, JoinTable, Rec, Where, Extras) -> erlydb_base:find_related_many_to_many(OtherModule, JoinTable, Rec, Where, Extras). aggregate_related_many_to_many(OtherModule, JoinTable, AggFunc, Rec, Field, Where, Extras) -> erlydb_base:aggregate_related_many_to_many(OtherModule, JoinTable, AggFunc, Rec, Field, Where, Extras). find_related_many_first(Func, Rec, Where, Extras) -> erlydb_base:find_related_many_first(Func, Rec, Where, Extras). find_related_many_max(Func, Rec, Num, Where, Extras) -> erlydb_base:find_related_many_max(Func, Rec, Num, Where, Extras). find_related_many_range(Func, Rec, First, Last, Where, Extras) -> erlydb_base:find_related_many_range(Func, Rec, First, Last, Where, Extras). aggregate_related_many(Func, AggFunc, Rec, Field, Where, Extras) -> erlydb_base:aggregate_related_many(Func, AggFunc, Rec, Field, Where, Extras). do_save(Rec) -> erlydb_base:do_save(Rec). do_delete(Rec) -> erlydb_base:do_delete(Rec). field_names_for_query() -> erlydb_base:field_names_for_query(musician). field_names_for_query(UseStar) -> erlydb_base:field_names_for_query(musician, UseStar). field_to_iolist(Val) -> erlydb_base:field_to_iolist(Val). set(Idx, Rec, Val) -> setelement(Idx, Rec, Val). db_pk_fields() -> erlydb_base:db_pk_fields([{erlydb_field, id, "id", <<105, 100>>, int, 11, integer, text_field, false, primary, undefined, identity}]). get_pk_fk_fields() -> erlydb_base:get_pk_fk_fields([{id, musician_id}]). get_pk_fk_fields2() -> erlydb_base:get_pk_fk_fields2([{id, musician_id1, musician_id2}]). db_fields() -> erlydb_base:db_fields([{erlydb_field, id, "id", <<105, 100>>, int, 11, integer, text_field, false, primary, undefined, identity}, {erlydb_field, name, "name", <<110, 97, 109, 101>>, varchar, 20, binary, text_field, true, undefined, undefined, undefined}, {erlydb_field, birth_date, "birth_date", <<98, 105, 114, 116, 104, 95, 100, 97, 116, 101>>, date, undefined, date, text_field, true, undefined, undefined, undefined}, {erlydb_field, instrument, "instrument", <<105, 110, 115, 116, 114, 117, 109, 101, 110, 116>>, enum, [<<103, 117, 105, 116, 97, 114>>, <<112, 105, 97, 110, 111>>, <<100, 114, 117, 109, 115>>, <<118, 111, 99, 97, 108, 115>>], binary, select, true, undefined, undefined, undefined}, {erlydb_field, bio, "bio", <<98, 105, 111>>, text, undefined, binary, text_area, true, undefined, undefined, undefined}]). db_field_names() -> erlydb_base:db_field_names([id, name, birth_date, instrument, bio]). db_field_names_str() -> erlydb_base:db_field_names_str(["id", "name", "birth_date", "instrument", "bio"]). db_field_names_bin() -> erlydb_base:db_field_names_bin([<<105, 100>>, <<110, 97, 109, 101>>, <<98, 105, 114, 116, 104, 95, 100, 97, 116, 101>>, <<105, 110, 115, 116, 114, 117, 109, 101, 110, 116>>, <<98, 105, 111>>]). db_num_fields() -> erlydb_base:db_num_fields(5). id(Rec) -> erlydb_base:get(3, Rec). id(Rec, Val) -> setelement(3, Rec, Val). name(Rec) -> erlydb_base:get(4, Rec). name(Rec, Val) -> setelement(4, Rec, Val). birth_date(Rec) -> erlydb_base:get(5, Rec). birth_date(Rec, Val) -> setelement(5, Rec, Val). instrument(Rec) -> erlydb_base:get(6, Rec). instrument(Rec, Val) -> setelement(6, Rec, Val). bio(Rec) -> erlydb_base:get(7, Rec). bio(Rec, Val) -> setelement(7, Rec, Val). new(name, birth_date, instrument, bio) -> {musician, true, undefined, name, birth_date, instrument, bio}. driver() -> erlydb_base:driver({erlydb_mysql, [{last_compile_time, {{1980, 1, 1}, {0, 0, 0}}}, {outdir, "apps/music/ebin"}, debug_info, report_errors, report_warnings, {erlydb_driver, mysql}]}). count(Field, Where, Extras) -> erlydb_base:aggregate(musician, count, Field, Where, Extras). count(Field) -> erlydb_base:aggregate(musician, count, Field, undefined, undefined). count(Field, Where) -> erlydb_base:aggregate(musician, count, Field, Where, undefined). count_with(Field, Extras) -> erlydb_base:aggregate(musician, count, Field, undefined, Extras). avg(Field, Where, Extras) -> erlydb_base:aggregate(musician, avg, Field, Where, Extras). avg(Field) -> erlydb_base:aggregate(musician, avg, Field, undefined, undefined). avg(Field, Where) -> erlydb_base:aggregate(musician, avg, Field, Where, undefined). avg_with(Field, Extras) -> erlydb_base:aggregate(musician, avg, Field, undefined, Extras). min(Field, Where, Extras) -> erlydb_base:aggregate(musician, min, Field, Where, Extras). min(Field) -> erlydb_base:aggregate(musician, min, Field, undefined, undefined). min(Field, Where) -> erlydb_base:aggregate(musician, min, Field, Where, undefined). min_with(Field, Extras) -> erlydb_base:aggregate(musician, min, Field, undefined, Extras). max(Field, Where, Extras) -> erlydb_base:aggregate(musician, max, Field, Where, Extras). max(Field) -> erlydb_base:aggregate(musician, max, Field, undefined, undefined). max(Field, Where) -> erlydb_base:aggregate(musician, max, Field, Where, undefined). max_with(Field, Extras) -> erlydb_base:aggregate(musician, max, Field, undefined, Extras). sum(Field, Where, Extras) -> erlydb_base:aggregate(musician, sum, Field, Where, Extras). sum(Field) -> erlydb_base:aggregate(musician, sum, Field, undefined, undefined). sum(Field, Where) -> erlydb_base:aggregate(musician, sum, Field, Where, undefined). sum_with(Field, Extras) -> erlydb_base:aggregate(musician, sum, Field, undefined, Extras). stddev(Field, Where, Extras) -> erlydb_base:aggregate(musician, stddev, Field, Where, Extras). stddev(Field) -> erlydb_base:aggregate(musician, stddev, Field, undefined, undefined). stddev(Field, Where) -> erlydb_base:aggregate(musician, stddev, Field, Where, undefined). stddev_with(Field, Extras) -> erlydb_base:aggregate(musician, stddev, Field, undefined, Extras). find() -> erlydb_base:find(musician, undefined, undefined). find(Where) -> erlydb_base:find(musician, Where, undefined). find_with(Extras) -> erlydb_base:find(musician, undefined, Extras). find_first() -> erlydb_base:find_first(musician, undefined, undefined). find_first(Where) -> erlydb_base:find_first(musician, Where, undefined). find_first_with(Extras) -> erlydb_base:find_first(musician, undefined, Extras). find_max(Max) -> erlydb_base:find_max(musician, Max, undefined, undefined). find_max(Max, Where) -> erlydb_base:find_max(musician, Max, Where, undefined). find_max_with(Max, Extras) -> erlydb_base:find_max(musician, Max, undefined, Extras). find_range(First, Max) -> erlydb_base:find_range(musician, First, Max, undefined, undefined). find_range(First, Max, Where) -> erlydb_base:find_range(musician, First, Max, Where, undefined). find_range_with(First, Max, Extras) -> erlydb_base:find_range(musician, First, Max, undefined, Extras).
With this decompiled file, you get all things clearly behind the magic, such as, you have pair getter/setter functions for each field, for example:
Musician = musician:find({name, 'like' "Caoyuan Mus"}), %% get the 'name' field of record Musician Name = musician:name(Musician), %% set the 'name' field to "new name" and bind to a new record Musician1. Musician1 = musician:name(Musician, "new name"), %% Or, Musician2 = musician:set_fields(Musician, {name, "new name"}, {birth_day, "1940/10/9"}), %% then save one of them musician:save(Musician2).
Finally, some notices for new comer to Erlang and Erlyweb:
- In Erlang, the Variable can only be bound(set value) once , so, only Musician1 and Musician2 have the "new name", Musician will keep the original value
- For efficiency reason, if the field is varchar/text type, the getter will return a binary rather than string, which can be printed on browser directly in Erlyweb, but, if you want to use it as a string, you can apply binary_to_list(Name) on it.
From Rails to Erlyweb - Part II
Updated Aug 23: Please see From Rails to Erlyweb - Part II Manage Project - Reloaded
Updated July 15: store the database configuration in <opaque> session of yaws.conf
Updated May 2: erlweb:compile(AppDir::string(), Options::[option()]) has option: {auto_compile, Val}, where Val is 'true', or 'false'. In case of development, you can turn on {auto_compile, true}. So, you only need to run erlyweb_app:boot(myapp) once.
2. Manage project
Erlyweb provides erlyweb:compile(App, ..) to compile the source files under app directory. To start an app, you usually should erlydb:start(mysql, ....) and compile app files first. To make life easy, you can put some scripting like code under myproject\script directory. Here's my project source tree:
myproject + apps | + myapp | + ebin | + include | + nbproject | + src | + components | + lib | + services | + test | + www + config | * yaws.conf + script + ebin + src * erlyweb_app.erl
Where, config/yaws.conf contains the confsiguration that will copy/paste to your real yaws.conf file. Here's mine:
## This is the configuration of apps that will copy/paste to your yaws.conf. ebin_dir = D:/myapp/trunk/script/ebin ebin_dir = D:/myapp/trunk/apps/myapp/ebin <server localhost> port = 8000 listen = 0.0.0.0 docroot = D:/myapp/trunk/apps/myapp/www appmods = </myapp, erlyweb> <opaque> appname = myapp hostname = "localhost" username = "mememe" password = "pwpwpw" database = "myapp_development" </opaque> </server>
You may have noticed, all beams under D:/myapp/trunk/script/ebin and D:/myapp/trunk/apps/myapp/ebin will be auto-loaded by yaws.
erlyweb_app.erl is the boot scripting code, which will be used to start db connection and compile the code. Currently I run these scripts manually. I'll talk later.
-module(erlyweb_app). -export([start/1]). -export([main/1, boot/1, build/1, decompile/2 ]). -include("yaws.hrl"). %% @doc call back funtion when yaws start an app %% @see man yaws.conf %% start_mod = Module %% Defines a user provided callback module. At startup of the %% server, Module:start/1 will be called. The #sconf{} record %% (defined in yaws.hrl) will be used as the input argument. This %% makes it possible for a user application to syncronize the %% startup with the yaws server as well as getting hold of user %% specific configuration data, see the explanation for the %%context. start(ServerConf) -> Opaque = ServerConf#sconf.opaque, AppName = proplists:get_value("appname", Opaque), Database = proplists:get_value("database", Opaque), DBConf = [{database, Database}, {hostname, proplists:get_value("hostname", Opaque)}, {username, proplists:get_value("username", Opaque)}, {password, proplists:get_value("password", Opaque)}], io:fwrite(user, "Starting app ~s using database:~n~s~n", [AppName, Database]), start_db(DBConf). start_db(DBConf) -> erlydb:start(mysql, DBConf). main([AppName]) -> boot(AppName); main(_) -> usage(). boot(AppName) -> build(AppName, true). build(AppName) -> build(AppName, false). build(AppName, AutoCompile) when is_atom(AppName) -> build(atom_to_list(AppName), AutoCompile); build(AppName, AutoCompile) when is_list(AppName) -> compile(AppName, [debug_info, {auto_compile, AutoCompile}]). compile(AppName, Options) -> % Retrieve source header paths from yaws server configuration. We don't know % how to get yaws.hrl here yet, so we manually write matching rule here. {ok, GC, _} = yaws_server:getconf(), {gconf, _, _, _, _, _, _, _, _, _, _, _, _, _, Incl, _, _, _, _, _} = GC, %?Debug("paths: ~p", [Incl]), erlyweb:compile( "./apps/" ++ AppName, [{erlydb_driver, mysql}, {i, Incl}] ++ Options). decompile(AppName, Beam) when is_atom(AppName) -> decompile(atom_to_list(AppName), Beam); decompile(AppName, Beam) when is_list(AppName) -> BinFilename = "./apps/" ++ AppName ++ "/ebin/" ++ atom_to_list(Beam), io:format("Beam file: ~s~n", [BinFilename]), {ok, {_, [{abstract_code, {_, AC}}]}} = beam_lib:chunks(BinFilename, [abstract_code]), SrcFilename = "./apps/" ++ AppName ++ "_" ++ atom_to_list(Beam), %% do not with ".erl" ext? otherwise will be compiled by ealyweb {ok, S} = file:open(SrcFilename ++ ".erl", write), io:fwrite(S, "~s~n", [erl_prettypr:format(erl_syntax:form_list(AC))]). usage() -> io:format("usage: erlyweb_app AppName\n"), halt(1).
To build it,
> erlc -I /opt/local/lib/yaws/include erlyweb_app.erl
The erlyweb_app.erl is almost escript ready, but I use it as module functions currently. It's pre-compiled and erlyweb_app.beam is placed under script/ebin
So, I start myapp by steps:
cd \myproject yaws -i -sname yaws 1> erlyweb_app:build(myapp).
The erlyweb_app.erl is almost escript ready, but I use it as module functions currently. It's pre-compiled and erlyweb_app.beam is placed under script/ebin
After I made changes to myapp, I run above erlyweb_app:build(myapp). again, then everything is up to date.
This is surely not the best way to handle the write-compile-run-test cycle, I'll improve the scripting to let starting yaws as a node, then hot-swap the compiled code to it.
It's a good experience to play with Rails, I like rake db:migrate, script, config folders of Rails. And Grails also brings some good idea to manage web app project tree. I'll try to bring them into project manager of ErlyBird.
Next part, I'll talk about the magic behind Erlyweb, and why I prefer the magic of Erlyweb to Rails.
ErlyBird Screenshot: Including Args in Completion Suggestion
As a newbie to Erlang, I'm not familiar with those OTP module/functions, I have to go back to see the docs again and again. At least, now ErlyBird will suggest me the arguments of each function now.
Click on the picture to enlarge it
Other progress:
- If erl/erl.exe is under the environment path, ErlyBird will try to set Erlang Installation path automatically.
- Indexing engine is now based on Lucene, and is integrated with NetBeans' Generic Scripting Framework 100%.
- As I'm now using ErlyBird for my daily works, and have written more Erlang code, I fixed some bugs that cause ErlyBird unstable.
To do list in the near future:
- Integrate with Erlware to provide better project manager.
- Go to declaration of macro
- Completion for fields of record
- Highlighting unbound variables and unused variables
- Basic re-factor features: rename local variable name, un-exported function name etc.
- Manage Erlyweb project
- Support syntax of Erlyweb template: *.et files
- Stabilize, Stabilize
And I hope language engine of NetBeans will be optimized soon, since it eats too much memory and needs performance improvement.
From Rails to Erlyweb - Part I
Updated July 20 2007: new params.erl which uses dict to store params and converts to proper type(integer/float/string)
It's time to migrate our project from Rails to Erlyweb (It cost me one month to write an Erlang IDE before this happens :-)). I'll blog some tips of the procedure, focus on the differences between Rails and erlyweb.
1.How to handle params
Rails:page = params.fetch(:p, 1).to_i size = params.fetch(:s, @@RECORDS_PER_PAGE).to_iErlyweb:
-module(mymodel_controller).. -define(RECORDS_PER_PAGE, 9). list(A) -> P = params:from_yaws_arg(A) Page = params:get("p", P, 1), %% return integer Size = params:get("s", P, ?RECORDS_PER_PAGE), ....
You can also
Id = params:get("id", P, integer) %% return integer() Id = params:get("id", P) %% return string() %% Or, pass a fun to return the converted result Id = params:get("id", P, fun string:to_integer/1)To get the above code working, I wrote a params.erl and placed under folder: apps\myapp\lib.
-module(params). -export([ from_yaws_arg/1, from_list/1, get_page_opts/1, get_page_extras/1, get/2, get/3, get/4, convert/2, put/3, join/1 ]). %% @doc Convert yaws Arg#arg.querydata into a {key,value} stored in a dynamic hash. from_yaws_arg(Arg) -> Params = yaws_api:parse_query(Arg), dict:from_list(Params). from_list(PropsList) -> dict:from_list(PropsList). get_page_opts(Dict) -> [{page, get("p", Dict, 1)}, {page_size, get("s", Dict, ?DEFAULT_PAGESIZE)}]. get_page_extras(Dict) -> Page = get("p", Dict, 1), Size = get("s", Dict, ?DEFAULT_PAGESIZE), Offset = (Page - 1) * Size, [{limit, Offset, Size}]. get(Key, Dict) -> get(Key, Dict, undefined). %% @spec get(Key::term(), Arg:yawsArg(), Default::term() -> string() get(Key, Dict, Fun) when is_function(Fun) -> case get(Key, Dict, undefined) of undefined -> undefined; Value -> Fun(Value) end; get(Key, Dict, Type) when is_atom(Type) andalso Type /= undefined -> get(Key, Dict, undefined, Type); get(Key, Dict, Default) -> case dict:find(Key, Dict) of {ok, Value} when is_list(Default) -> convert(Value, string); {ok, Value} when is_integer(Default) -> convert(Value, integer); {ok, Value} when is_float(Default) -> convert(Value, float); {ok, Value} -> Value; error -> Default end. get(Key, Dict, Default, Type) -> Value = get(Key, Dict, Default), convert(Value, Type). convert(Value, _Type) when Value == undefined -> undefined; convert(Value, Type) -> try case Type of integer when is_list(Value) -> list_to_integer(Value); integer when is_integer(Value) -> Value; integer when is_float(Value) -> erlang:round(Value); float when is_list(Value) -> list_to_float(Value); float when is_integer(Value) -> Value * 1.0; float when is_float(Value) -> Value; string when is_list(Value) -> Value; string when is_integer(Value) -> integer_to_list(Value); string when is_float(Value) -> float_to_list(Value); number when is_list(Value) andalso Value /= "" andalso Value /= "NaN" -> case string:chr(Value, $.) > 0 of true -> list_to_float(Value); false -> list_to_integer(Value) end; number when is_integer(Value) -> Value; number when is_float(Value) -> Value end catch error:_ -> undefined end. put(Key, Value, Dict) -> dict:store(Key, Value, Dict). join(Params) -> ParamPairedStrs = dict:fold( fun (Key, Value, Acc) -> [Key ++ "=" ++ yaws_api:url_encode(Value)|Acc] end, [], Params), %% Join params string with "&": ["a=a1", "b=b1"] => "a=a1&b=b1" join("&", ParamPairedStrs). %% @spec join(string(), list(string())) -> string() join(String, List) -> join(String, List, []). join(_String, [], Joined) -> Joined; join(String, [H|[]], Joined) -> join(String, [], Joined ++ H); join(String, [H|T], Joined) -> join(String, T, Joined ++ H ++ String).
Write an IDE in One Month - ErlyBird 0.11.0 Released
Updated Apr 24: The indexing feature is based on Lucene indexing from Common Scripting Framework now. But there won't be new release soon, since Generic Language Framework changes rapidly.
Updated Apr 21: There are several source files under \lib\megaco-3.5.3\src\text, which are with size > 300k, if you can not pass indexing procedure (ErlyBird hangs), please rename them something that are not end with ".erl" or ".hrl".
Updated Apr 20: Due to a severe bug that prevents setting Erlang Installation path, I've re-pack a new release 0.11.1 that fixed it. Don't forget to set the Erlang Installation path to full path of erl.exe or erl file, for instance, "C:\erl\bin\erl.exe".
Updated Apr 20: There is a bug, if your OTP is not installed as C:\erl, you can not set Erlang Installation. I'll update the bin package.
Updated Apr 20: erlybird-bin-0.11.0-ide.zip has been uploaded to sourceforge.net.
I'm newbie to IDE, to Erlang, to compilers principles. But, based on the works from NetBeans's guys, I wrote an almost complete Erlang IDE in one month.
With features:
- Syntax checking.
- Syntax highlighting.
- Functions navigator.
- Code-folding.
- Indentation.
- Completion for built-in/remote function call. (Ctrl+Space for suggestion) (new)
- Go to declaration for remote function call (press down Ctrl and click on the function name). (new)
- Project management - create/manage/build project tree. (new)
- Compilation error locate. (new)
- Auto indexing OTP libs and project sources. (new)
- Interactive Erlang Console. (new)
And with known Issues:
- ErlyBird is memory eager so far, it needs at least -Xmx256m set, 500m or more is recommended for big source files. Check the config file of NetBeans that is located at etc/netbeans.conf, make sure you've set -J-Xmx256m in line of 'netbeans_default_options'
- Run project button does not work yet, if you press run project button, will show an interactive erlang shell only.
- When indexing OTP libs, there may be Exceptions pop-ups, which indicate out of heap space, you can just ignore it.
- Do not open/go to declaration too big source file in ErlyBird, this will also cause out of heap space.
And lacking features:
- Debugging
- ...
The parsing and editing features is based on Generic Language Framework.
The project management and indexing features is based on Common Scripting Framework.
To integrate editing feature, which is from Generic Language Framework, with Comman Scripting Framework, I had to lightly modify the Common Scripting Framework source code, the modified files are available from svn trunk folder: gsf-diff-ref.
And as I'm not yet familiar with Lucene index engine, which is used by Common Scripting Framework, I just wrote a sql db index engine and plug-in it to Common Scripting Framework. I will rewrite it based on LuceneIndex lately.
To download, please go to:
http://sourceforge.net/project/showfiles.php?group_id=192439
There are two installation options now, you can choose one of them:
The first one: A pre-packed NBMs kit: erlybird-bin-0.11.1-kit-nbms.zip(about 2.8M). To install:
Downloaded NetBeans IDE 6.0 M8+ via:
http://www.netbeans.info/downloads/dev.php
Select 'Milestone' in 'Build Type'.
After NetBeans IDE installed, unzip erlybird-bin-0.11.1-kit-nbms.zip first, then:
- From menu: Tools -> Update Center
- In the "Select Location of Modules" pane, click "Install Manually Downloaded Modules(.nbm Files)", then "Next"
- Click [Add...] button, go to the path to select all *.nbm files.
- Following the instructions to install updated modules.
- Restart NetBeans.
- Set your OTP path. From [Tools]->[Options], click on 'Miscellanous', then expand 'Erlang Installation', fill in the full path of your 'erl.exe' or 'erl' file, for instance: "C:/erl/bin/erl.exe"
The second one: A standalone ErlyBird IDE: erlybird-bin-0.11.1-ide.zip(about 18M). Notice: Please wait for me to upload it to sf.net :-) It does not need NetBeans IDE. To install:
- Just unzip it to somewhere, then execute 'bin/erlybird.exe' for windows, 'bin/erlybird' for *nix.
- Set your OTP path. From [Tools]->[Options], click on 'Miscellanous', then expand 'Erlang Installation', fill in the full path of your 'erl.exe' or 'erl' file, for instance: "C:/erl/bin/erl.exe"
If you are new to NetBeans, there are some docs for user:
http://www.netbeans.org/kb/55/quickstart.html
http://www.netbeans.org/kb/55/using-netbeans/index.html
It may not be stable yet, feedback and bug reports are welcome.
ErlyBird Screenshot: Completion/Go to Declaration for remote function call
ErlyBird now support completion for remote function call. And, if you click on the name of a remote function call, ErlyBird will open the remote module source file, and jump to the position of declaration. (Only OTP modules are supported currently)
The screenshot shows the popup with candidate functions of module "file", when you typing "file:"
I'll release a new version in one week.
Click on the picture to enlarge it
ErlyBird Screenshot: Erlang Console and File Locator for Compile Errors
Updated Apr 11: copy/paste can be done via Ctrl+C/Ctrl+V.
The Erlang Shell console finally works on NetBeans, it works as same as on the shell/dos environment with historical commands feature. It still lacks copy/paste feature.
And ErlyBird supported Emakefile based project build. In the output window, you can click on the compiler error message to jump to the corresponding location of source file.
Click on the picture to enlarge it
Erlang Project Support and Code Completion in ErlyBird
I've got the initial Erlang project management supported in ErlyBird, where the Erlang project tree can be newly created and managed by NetBeans. The code is ported from Tor's work for Ruby in NetBeans, and I'll try to sync my work with his work.
The Erlang interactive console does not truly work yet:-) but it's the first priority task.
Click on the picture to enlarge it
Go to Declaration of Function call and Var in Erlang Editor for Netbeans
I've got "Go to declaration of function call and var" if the declarations are in the same module file, and "Highlighting for function call/function arguments" working.
To go to the declaration of function call or var, just press down "Ctrl", and put cursor on the function call or var name's position, then click on it. The editor will jump to the source position of declaration.
But to get cross-module "Go to declaration of function call" working, I may need much more works to do.
BTW, the Erlang project management is also under developing. Before this feature is released, the only method to create a managed project in NetBeans is create a Java project tree and use it.
Click on the picture to enlarge it
Three Interviews with Language Programmers for NetBeans
Geertjan from NetBeans took three interviews with language programmers for NetBeans, one of them is me, where I talked about ErlyBird - Erlang Editor for NetBeans and AIOTrade (formly HumaiTrader) which are all based on NetBeans Platform.
About AIOTrade, in the interview, I said:
"I'm going to split AIOTrade into a client/server application. The server-side will act as a streaming quote data feed server and be responsible for delivering transaction orders to brokers in soft real-time, written in Erlang. The client-side charting and UI features will remain in Java, where Java is strong. With the Jinterface APIs provided by Erlang, Java is easily able to talk with the Erlang server."
Erlang Editor for NetBeans - ErlyBird 0.10.1 released
Update - Mar 29,2007: If you got exception: java.lang.reflect.InvocationTargetException when try completion, please check the version number of your "Generic Languages Framework" module (Tools -> Module Manager -> Language Support), if the version number is less than 1.70, you can go to http://sourceforge.net/projects/erlybird to download and update to the newly built org-netbeans-modules-languages.nbm
I'm pleased to announce ErlyBird 0.10.1, an Erlang Editor Module for NetBeans has been released.
Current features:
- Syntax checking;
- Syntax highlighting;
- Functions navigator;
- Code-folding;
- Indentation;
- Built-in function completion.
You can download ErlyBird from http://sourceforge.net/projects/erlybird
ErlyBird needs NetBeans IDE 6.0 M7+, which can be downloaded via:
http://www.netbeans.info/downloads/dev.php page
select Q-Build in 'Build Type'.
After NetBeans IDE installed, go to Tools->Update Center, fetch the "Generic Language Framework" module from Category "Languages Support"
To install ErlyBird module, unzip the binary package first, then:
- From menu: Tools -> Update Center
- In the "Select Location of Modules" pane, click "Install Manually Downloaded Modules(.nbm Files)", then "Next"
- Click [Add...] button, go to the path to select the unzip .nbm file.
- Following the instructions to install updated modules.
- Restart NetBeans.
It may not be stable yet, feedback and bug reports are welcome.
Erlang Editor Support Based on NetBeans' Generic Language Framework
I did some work to get Erlang editor supported on NetBeans. As the Schliemann project (Generic Language Framework) is still under developing, I just got simple syntax coloring, indentation, code folding etc. working. I'll contribute it to NetBeans Community when it's stable enough. Here is a snapshot:
Click on the picture to enlarge it
Some Tips for Upgrading to Rails 1.2.x
Bellow are some issues that I met when upgraded from rails 1.1.6 to 1.2.x:
1.About enrivonment.rb
Make sure your application configuration behind:
Rails::Initializer.run do |config| ... end
I met this issue when I put following code:
require 'environments/localization_environment' require 'localization' Localization::load_localized_strings require 'environments/user_environment'
before that closure, which works under 1.1.6, but don't under 1.2.x
2.About ActionMailer::Base.server_settings
If you get errors like:
uninitialized constant ActiveSupport::Deprecation::RAILS_DEFAULT_LOGGER (NameError)
try to change your ActionMailer::Base.server_settings to ActionMailer::Base.smtp_settings
3.Put all "include" statements inside class definitions
You must put include statements inside class/module definitions instead of outside a class/module definition in Rails 1.2.x. Otherwise, you'll get:
Error calling Dispatcher.dispatch #<NameError: cannot remove Object::COPYRIGHT>
Ruby IDE for NetBeans Almost Useful
As NetBeans IDE 6.0M7 released, I tried the Ruby module for it, and it's almost useful now.
To get and install,
1. Downloand NetBeans IDE 6.0M7 from:
http://www.netbeans.info/downloads/dev.php
Select 'Q-Build' and download the newest M7
2. Update Ruby modules:
1) [Tools] -> [Update Center]
2) Select Ruby folder as you wanted (9 files will be selected)
3) Following the instructions.
3. Set your Ruby environment:
As the default installation will use JRuby, if you want to use c-ruby, go to
1) [Tools]->[Options]->Miscellaneous->Ruby Installation
2) Change all ruby tools to yours
4. Now setup your first Ruby on Rails Application:
1) [File]->[New Project]->Ruby->Ruby on Rails Application
2) If you have an existed project, copy and override to the new created project tree.
Want to take a look at the snapshot? here it is:
NetBeans + Ruby = True
That's all. Have fun with great NetBeans.
Notice: If you are using c-ruby, don't try to run project via NetBeans' "run main project" button, which may change your environment temporarily.
AIOTrade: Yahoo! data module updated
Due to the recent date format change in the Yahoo! historical data, I've updated the relative module: org-aiotrade-platform-modules-dataserver-basic.nbm
And also updated module: org-aiotrade-math.nbm to fix the bug mentioned at: Solution for bug: wrong date shown
To update your installed Aiotrade 1.0.3:
1.From menu: Tools -> Update Center
2.In the "Select Location of Modules" pane, click "Install Manually Downloaded Modules(.nbm Files)", then "Next"
3.Click [Add...] button, go to the path to select these two .nbm files that you downloaded. (Press Ctrl key down and click on file name to select multiple files)
4.Following the instructions to install updated modules.
5.Restart Aiotrade.
!Notice, you may need to delete all aiotrade log files, which are located at:
For Windows user:
C:\Documents and Settings\yourusername\Application Data\.aiotrade\1.0.3\var\log\
For Mac user:
${HOME}/Library/Application Support/aiotrade/1.0.3/var/log/
This is a quick fix, the final solution will be included in next release.
Functinal Style Ruby
After playing with Ruby for weeks, I found Ruby is yet interesting. I try to write my code a bit in the likeness of Erlang, where symbol vs atom, array vs list. And the most important syntax that I like are:
- everything return a value
- may return multiple values
- begin-end clause is lambda that may be directly applied
- parallel assignment
Now, let's write some code before and after (functional):
Example1:
Before
1. cond = {} 2. if par[:id] 3. feed = Feed.find(par[:id]) 4. if feed 5. cond[:feed] = feed.id 6. end 7. end 8. if par[:m] 9. limit = par[:m].to_i 10. else 11. limit = 20 12. end 13. if limit >= 4096 14. limit = 4096 15. end 16. cond[:limit] = limit 17. if par[:d] 18. days = par[:d].to_f 19. if days <= 0 || days >= 365 20. days = 365 21. end 22. cond[:time] = Time.now - days*86400 23. endAfter
1. cond = { 2. :feed => if par[:id] 3. feed = Feed.find(par[:id]) 4. feed ? feed.id : nil 5. end, 6. :limit => begin 7. limit = par[:m] ? par[:m].to_i : 20 8. limit >= 4096 ? 4096 : limit 9. end, 10. :time => if par[:d] 11. days = par[:d].to_f 12. days = days <= 0 || days >= 365 ? 365 : days 13. Time.now - days * 86400 14. end, 15. }.delete_if { |k, v| v.nil? } # delete all nil elements of cond
Example2:
Before
1. if f[:mode] == "rss" 2. rss = f[:feed] 3. params[:feed][:channel] = rss.channel.title 4. params[:feed][:description] = rss.channel.description 5. params[:feed][:link] = rss.channel.link 6. params[:feed][:copyright] = rss.channel.copyright 7. else 8. atom = f[:feed] 9. params[:feed][:channel] = atom.title 10. params[:feed][:description] = atom.subtitle 11. params[:feed][:link] = atom.links.join 12. params[:feed][:copyright] = atom.rights 13. endAfter
1. params[:feed][:channel], 2. params[:feed][:description], 3. params[:feed][:link], 4. params[:feed][:copyright] = if f[:mode] == "rss" 5. rss = f[:feed] 6. 7. [rss.channel.title, 8. rss.channel.description, 9. rss.channel.link, 10. rss.channel.copyright] 11. else 12. atom = f[:feed] 13. 14. [atom.title, 15. atom.subtitle, 16. atom.links.join, 17. atom.rights] 18. end
Example3
1. # grp_str: p -> public(0) , u -> user(1), f -> friends(2) 2. def privilege_cond(user, grp_str) 3. grp_str ||= 'puf' 4. cond = {:pre => "", :sub => []} 5. cond = if loggedin?(user) 6. frds = grp_str.include?('f') ? user.friends.find(:all) : [] 7. frd_ids = frds.collect { |frd| frd.friend_id.to_i } 8. 9. cond = if grp_str.include?('u') 10. {:pre => cond[:pre] + (cond[:pre] == "" ? "" : "OR") + 11. " user_id = ? ", 12. :sub => cond[:sub] + [user.id]} 13. else 14. cond 15. end 16. 17. cond = if grp_str.include?('f') && !frd_ids.empty? 18. {:pre => cond[:pre] + (cond[:pre] == "" ? "" : "OR") + 19. " user_id in (?) AND privilege in (?) ", 20. :sub => cond[:sub] + [frd_ids, [0, 2]]} 21. else 22. cond 23. end 24. 25. cond = if grp_str.include?('p') 26. {:pre => cond[:pre] + (cond[:pre] == "" ? "" : "OR") + 27. " user_id != ? AND privilege = ? ", 28. :sub => cond[:sub] + [user.id, 0]} 29. else 30. cond 31. end 32. else 33. {:pre => "privilege = ?", 34. :sub => [0]} 35. end 36. end
Java + Ruby + Erlang = JRE (Just Running Environment)
I'm recently doing a project under Ruby on Rail. It seems to be a reasonable programmer today, one should take at least > 3 languages.
Personally,
I like Erlang: lightweight process + message passing + functional programming + dynamic. It exactly matches my philosophy of looking the real world, and I think it's what functional programming should be.
Are there really Objects exist? I'm not sure. Instead, talking about OO, Object Oriented may be more sense. That is, an Object makes sense only when you orient it. All states look like being "in" an object, are with meaning only when you measure them. But, doesn't "measure" mean applying a "Function" on it? So, the states should always be carried only by functions rather than 'object', and the states are time streaming, they will be transfered from one function to another function, another function ..., so you catch the meaning of them when you track the functions chain, the meaning is based on the functions rather than the name of a Class as a member of. When you want to take a snapshot on them, you save them some where, such as showing on screen, stored in database, printed on paper what ever.
I like Java: tons of APIs + open source code base + Swing + NetBeans. So far, it has the best cross-platform UI tool kit to my eye. I like Swing, I can change or extend it easily to whatever I want. But things go easy because so many people have taken extremely efforts on it. It's bound too Objected, people split world to objects, then try to composite them or inherit something called super to make them together again. I feel pain when doing this, I have to split them, composite them in a way, then things change (or, the real world is still there), I split them, composite them in another way, again and again, it's called re-factor, but people may never catch the real Factor of the real world.
I, have to learn Ruby. Ruby and Rails are very good. For developers term, you should always know things are not so philosophy as yours, you will have guys thinking in different ways, of the real world. So Ruby is there, everyone can think the real world according to his understanding, yeah, in different ways, and, to make them not going too far away, you need rails.
So, I have to learn Java, as a tool make me doing many things interesting and painful; I have to learn Ruby, as a tool make my guys doing many things interesting and on rails; And I'll keep Erlang (Lisp/Scheme) as a tool make me not only doing but also thinking with interesting.
Split the Server Part and Client Part? - Next Generation AIOTrade
I'm thinking to re-design the AIOTrade by splitting it to Server part and Client part. The server part may also run as a standalone web application.
But, I'm also busy on another project, so, the prototype and coding work will be postponed.
No Static Method in Interface, So I Write Code As ...
Java does not support static method in interface. But sometimes, I just want a static method to say: PersistenceManager.getDefault(), where PersistenceManager is going to be an interface. I don't like to add one more class named PersistenceManagerFactory, with a method:
public static PersistenceManager PersistenceManagerFactory.getDefault()
So I write code like:
public class PersistenceManager { private static I i; public static I getDefault() { return i == null ? i = ServiceLoader.load(I.class).iterator().next() : i; } /** The interface I, which is actually the PersistenceManager should be: */ public static interface I { void saveQuotes(String symbol, Frequency freq, Listquotes); ListrestoreQuotes(String symbol, Frequency freq); void deleteQuotes(String symbol, Frequency freq, long fromTime); void dropAllQuoteTables(String symbol); void shutdown(); QuotePool getQuotePool(); TickerPool getTickerPool(); } }
Then implement the PersistenceManager.I in another package, like:
public class NetBeansPersistenceManager implements PersistenceManager.I { ... }
And declare it under the META-INF as:
core/src/META-INF/services/org.aiotrade.math.PersistenceManager$I
which contains one line:
org.aiotrade.platform.core.netbeans.NetBeansPersistenceManager
I can call PersistenceManager.getDefault().showdown() now.
Prediction 4 Months Ago and Actual Trends Today, by Neural Network
Today, Shanghai Security Index ( 000001.SS) touched 2100, and, from my previous neural network research on 0000001.SS, about 4 months passed. In that blog, I placed a prediction picture, and now, here it's a verification picture of the actual price trends comparing to the prediction:
The Blue line is the prediction calculated by Neural Network. The Red/Green line is the actual price trends.
Click on the picture to enlarge it
Solution for Bug: wrong date shown
There is a bug affects those who's time zone is not UTC:
For each stock, AIOTrade shows the data of the day after. For example, the data of Friday November the 24th is shown in the application at Friday November the 23th.
This bug will be fixed in next release, by far, you can try to add a a property in aiotrade's config file, which is located at:
path.to.installation/etc/aiotrade.conf
change the line:
default_options="-J-Xms24m -J-Xmx128m --fontsize 11"
to:
default_options="-J-Xms24m -J-Xmx128m --fontsize 11 -J-Duser.timezone=UTC"
Sorry about the inconvenience.
AIOTrade 1.0.3 Released
I'm pleased to announce the 1.0.3 release of AIOTrade. You may download it via Sourceforge.net
The new features had been listed in my previous blog
Thanks for all the suggestions, bugs reports and feature requests. Thanks for msayag's bugs hunt.
Bug reports, especially for the experimental IB-TWS data feed are welcome.
What's new in coming AIOTrade release
After one and half month developing, the next release of AIOTrade is on the road. Here is the new features and improvements in the coming release:
- Charting performance was improved a lot.
- Added an integrated scrolling, zooming controller. Zoom out/in by dragging the thumb side to any scope you want.
- Added an icon beside the symbol, indicating the data source, such as from Yahoo!/CSV etc.
- When you switch to different frequency charting , only the indicators and drawings with the same frequency displayed under the symbol node.
- The symbols under watching will have its real-time charts displayed on a scrolling window. Double click on its title will pop up a standalone window.
- Some times, too many indicators are displayed in chart view, you can double-click on indicator's title to pop up a standalone indicator window for detailed analysis. There will be a scrolling controller attached in this window, and shares the same cursor with the main chart view.
- An experimental IB-TWS data feed interface. As I can only test it on demo environment, this feature is just to be released for wide range testing, I'm even not sure if it really works:-), so please feed me back all the issues, I'll fix them and release an official version as soon as possible.
- Save chart to custom time frame image, even bigger than your screen.
And another important question: Where are the AI features? Sorry, as I have no much time, although I'll keep my own research, the open-source AI features will be pended. Until ... I don't know.
The scheduled release date is around Nov 19.
Click on the picture to enlarge it
A big chart of DJI from Jan 2000 till now:
Way To Going
Recently I got some messages that worried about the way to go for AIOTrade project, as eneratom posted in sf.net forum. Good question, as I am also thinking about it.
Although the initial motivation for this project is building a platform for myself research on AI on trading, I finally find I've gone a bit far from it, it's now a platform with potential to be All In On trading platform.
I were busy on my own affairs in the passed months, such as moving, looking for job (maintaining an open-source project is a bit hard) etc. When I come back to this project these days, I find I'm still with passion on it. So the svn trunk is being filled with a lot of new committed works which will bring the architecture to a much more beauty and clean: such as a more powerful custom scrollbar to control scrolling/zooming all in one with auto-hide ability, a XOR mouse cursor with better performance etc.
The answer for me now is clear: it will be an All In One Trading platform with AI features.
Let me try to give a road map for the near future:
- The next release will be with a lot of fine tuning on performance and a neural network module.
- The next next release will support mins data loading and more data feeds
- The next, will implement back-testing features.
- Then, an interface for placing orders to brokers
- ...
For the contributions, I'm very appreciate for the suggestions, but as I mentioned before, there will not be more developers until the APIs are stable enough, and all the license issues have been considered carefully.
The patches and bug reports are always welcome though.
A Regress Bug in java.awt.geom.Path2D (JDK 6.0 beta 2)
I tested AIOTrade on newly downloaded JDK 6.0 beta 2, and got an exception instantly:
java.lang.ArrayIndexOutOfBoundsException: 0 at java.awt.geom.Path2D$Float.moveTo(Path2D.java:322) at java.awt.geom.Path2D$Float.append(Path2D.java:643) at java.awt.geom.Path2D.append(Path2D.java:1780)
The code run good in JDK 5.0, so will it be a regress bug in JDK 6.0?
I then checked the source code: 6.0 vs 5.0, and found there were likely a bit of code omited wrongly. That is, in method body of void needRoom(boolean needMove, int newCoords), should add
if (grow < 1) { grow = 1; }at the next of:
int size = pointTypes.length; if (numTypes >= size) { int grow = size; if (grow > EXPAND_MAX) { grow = EXPAND_MAX; }
The following is the proper code I've tested OK:
void needRoom(boolean needMove, int newCoords) { if (needMove && numTypes == 0) { throw new IllegalPathStateException("missing initial moveto "+ "in path definition"); } int size = pointTypes.length; if (numTypes >= size) { int grow = size; if (grow > EXPAND_MAX) { grow = EXPAND_MAX; } /** fix bug: * java.lang.ArrayIndexOutOfBoundsException: 0 * at java.awt.geom.Path2D$Float.moveTo(Path2D.java:322) * at java.awt.geom.Path2D$Float.append(Path2D.java:643) * at java.awt.geom.Path2D.append(Path2D.java:1780) */ if (grow < 1) { grow = 1; } pointTypes = Arrays.copyOf(pointTypes, size+grow); } size = floatCoords.length; if (numCoords + newCoords > size) { int grow = size; if (grow > EXPAND_MAX * 2) { grow = EXPAND_MAX * 2; } if (grow < newCoords) { grow = newCoords; } floatCoords = Arrays.copyOf(floatCoords, size+grow); } }
As I can not wait for it be fixed in JDK, so I wrote another org.aiotrade.util.awt.geom.Path2D and org.aiotrade.util.awt.geom.GeneralPath, and replaced the java.awt.geom.GeneralPath in my source tree. you can get the code at:
It Will Be My First Attendance at NetBeans Day, Seattle
I will be there, Sun Tech Days, Seattle, Sep 6, 2006. As I'm now in Vancouver, it's about 2 or 3 hours trip to Seattle.
I'm glad to have a chance to meet those great guys who develop NetBeans IDE and Platform. As you know, the AIOTrade (formerly Humai Trader) is built on NetBeans Platform using NetBeans IDE.
And, I've committed the re-packed source code to SVN repository on sourceforge.net, and am doing cleanup on the neural network code, hope to commit the code in one week.
For the neural network module, there should be a lot of UI works still needed to be done, I've been beginning to hack the Visual Library API of NetBeans, and hope to apply these great works on visual neural network definition.
AIOTrade - New Name for BlogTrader Platform
BlogTrader Platform (i.e HumaiTrader) has got an official name as AIOTrade now, which means Artificial Intelligence On Trade, or, All In One Trade. And it will be a fairly large project to be developed.
Did I mentioned this project is one-year old now? and more than 10,000 copies (binary and source) was downloaded.
I've re-packed the source tree, and will release the code in name of AIOTrade soon, with a primary Neural Network module.
It's brought to you by AIOTrade Computing Co.
And the new web site:
http://www.aiotrade.com
or
http://www.aiotrade.org
is coming soon.
Support Vector Machine for BlogTrader Platform
I've got the Support Vector Machine (SVM) running on BlogTrader Platform. The results are more stable than MLP and I'm still being familar with the C and sigma parameters of SVR.
SVM is a good tool that based on Kernel and Statistical Learning theory, and it can reach the globle minima, with only few parameters needed to be adjusted which may be easily applied GA.
For those who are interesting in machine learning, there are several good books are recommented:
Statistical Learning Theory [Vladimir N. Vapnik]
An intruduction to Support Vector Machines and Other Kernel-based Learning Methods [Nello Cristianini, John Shawe-Taylor]
Kernel Methods for Pattern Analysis [John Shawe-Taley, Nello Cristianini]
Is Neural Network Useful to Analyse Stock Market?
I'm busy on cleanup the Neural Network code after World Cup. Now the performance is acceptable, momentum and rprop learner for MLP were done too. RBF Network almost works.
The Neural Network is not magic, I just use it to do non-linear regression. The results that I got are totally based on my unserstanding to stock market other than neural network.
Anyway, as the primary motivation for BlogTrader? project, the neural network and other machine-learning tools (such as SVM) will be continually under developing. I hope to release next version of BlogTrader? Platform in later September. Maybe a sample neural network indicator will be included in this coming version. If you are interesting in these approachs, you can write your own indicator based on NN.
Again, I try my understanding on stock market for 000001.ss, here's a result just for joy:
The yellow line is output of neural network in training period, the BLUE line is the prediction for un-trained period, the prediction uses none high-low-open-close values of predicion period, it's a mid-term prediction:
Click on the picture to enlarge it
Prediction on Another Stock Index by Neural Network
This time, let's try to apply the neural network on Shanghai Stock Exchange Index(000001.SS on Yahoo! Finance). The network has 2 hidden layers, the 1st one has 5 nodes, the 2nd one has 2 nodes. Output layer has 1 node.
After 5000 epoches training with 2400 sample data, the result is showing below.
It seems the pattern of chart is better than the turning time. Particularly, the backward generalization gets a very interesting result.
When I do the prediction, I do not use the price in the prediction period to adjust the network or as input any more. That is, I only use the price data in the training period to recognize the pattern, then apply the pattern backward or forward directly. So the prediction is fairly a long term prediction.
Click on the picture to enlarge it
A More Interesting Prediction of Stock Price by Neural Network
After more tuning work on my very primary neural network to predict the Price of stock market, I got some interesting result on ^HSI. 3600 sample data were used to train the network this time, from Mar 9, 2005, backward 3600 days. Generalization was applied forward from Mar 10, 2005 to Aug 23, 2006.
It's a simple MLP network with 3-layers, the hidden layer has 10 nodes, and 1 node for output layer.
Here's the result:
Click on the picture to enlarge it
And the zoom-in view, the result is also applied to the future (From Mar 10, 2005 to Aug 23, 2006 on this picture)
A Very Primary Prediction of Stock's Trends by Neural Network
I've designed the 1st version of my very primary neural network to predict the Trends and Turns (not the Price) of Stock market on BlogTrader Platform. 400 sample data were used to train the network, and generalization was applied forward and backward.
Here's predicted result(very primary):
Click on the picture to enlarge it
Trying of Neural Network for BlogTrader Platform
I'm writing the Neural Network module for BlogTrader Platform. The first implementation is a Multi-Layer Perceptron. I'm glad that the MLP is going to compute something out for me today.
Training a neural network is a really tricky work, it seems we need a lot of interaction with the results and parameters.
A typical training with 200 training data, 3-10-1 topological MLP network runs about 4 minutes for 5000 epochs at this time. I do not apply any performance optimizing on it till now.
Install XWiki on Glassfish and Derby
I've upgraded blogtrade.org to glassfish b48, with XWiki and javadb (Apache Derby) integrated. Here is a short guide I posted:
http://blogtrader.org/wiki/bin/view/KnowledgeBase/XWikiGlassfishDerby
Primary support for Buy/Sell Signals in BlogTrader Platform
The primary support for Buy/Sell signals has been implemented in BlogTrader Platform, inculding SignalChart, SignalIndictor etc.The signal indicators will be put in a separate module: "BlogTrader Platform Modules Basic Signal"
Below is a screenshot of signals generated by MACD's slow line cross fast line. The blue arrows indicate the signals:
The sample code:
public class MACDSignal extends AbstractSignalIndicator { Opt periodFast = new DefaultOpt("Period EMA Fast", 12.0); Opt periodSlow = new DefaultOpt("Period EMA Slow", 26.0); Opt periodSignal = new DefaultOpt("Period Signal", 9.0); Varfast = new DefaultVar(); Var slow = new DefaultVar(); { _sname = "MACD Signal"; _lname = "Moving Average Convergence/Divergence Signal"; } void computeCont(int begIdx) { for (int idx = begIdx; idx < _dataSize; idx++) { fast.set(idx, macd(idx, C, periodSlow, periodFast)); slow.set(idx, ema(idx, fast, periodSignal)); if (crossOver(idx, fast, slow)) { signal(idx, Sign.EnterLong); } if (crossUnder(idx, fast, slow)) { signal(idx, Sign.ExitLong); } } } }
Design of Function of Technical Analysis (Part I)
As the basic atom for indicator computation, the function design for TA should consider:
- It can be computed at any idx or time point, whenever -- computeSpot(int idx) or computeSpot(long time)
- It can correspond to the Options change (Options are transfered in by the caller as reference)
- It can remember the intermedial status of function instance to avoid redundant computation. The status includes
- backward values (as it's a TimeSeries's variable)
- the based Variables
- the Options
- the caller
Then an indicator instance can call it to:
- compute from 'begIdx' to 'endIdx'
- compute at point 'idx'
Supported TA Functions Currently
Below are the TA functions that have been implemented. The corresponding indicators were re-written based on these functions too.
More functions will be listed here as they are implemented.
float max(int idx, Var var, Opt period) float min(int idx, Var var, Opt period) float sum(int idx, Var var, Opt period) float ma(int idx, Var var, Opt period) float ema(int idx, Var var, Opt period) float stdDev(int idx, Var var, Opt period) float tr(int idx) float dmPlus(int idx) float dmMinus(int idx) float diPlus(int idx, Opt period) float diMinus(int idx, Opt period) float dx(int idx, Opt period) float adx(int idx, Opt periodDi, Opt periodAdx) float adxr(int idx, Opt periodDi, Opt periodAdx) float bollUpper(int idx, Var var, Opt period, Opt alpha) float bollLower(int idx, Var var, Opt period, Opt alpha) float bollMiddle(int idx, Var var, Opt period, Opt alpha) float cci(int idx, Opt period, Opt alpha) float macd(int idx, Var var, Opt periodSlow, Opt periodFast) float mfi(int idx, Opt period) float mtm(int idx, Var var, Opt period) float obv(int idx) float roc(int idx, Var var, Opt period) float rsi(int idx, Opt period) float sar(int idx, Opt initial, Opt step, Opt maximum) Direction sarDirection(int idx, Opt initial, Opt step, Opt maximum) float stochK(int idx, Opt period, Opt periodK) float stochD(int idx, Opt period, Opt periodK, Opt periodD) float stochJ(int idx, Opt period, Opt periodK, Opt periodD) float wms(int idx, Opt period) float[][] probMass(int idx, Varvar, Opt period, Opt nInterval) float[][] probMass(int idx, Var var, Var weight, Opt period, Opt nInterval) -------------------------------------------------------------- boolean crossOver(int idx, Var var1, Var var2) boolean crossOver(int idx, Var var, float value) boolean crossUnder(int idx, Var var1, Var var2) boolean crossUnder(int idx, Var var, float value) boolean turnUp(int idx, Var var) boolean turnDown(int idx, Var var) -------------------------------------------------------------- signal(int idx, Sign sign, String name) signal(int idx, Sign sign) enum Sign { EnterLong, ExitLong, EnterShort, ExitShort; } --------------------------------------------------------------
Switch to GPL
BlogTrader Platform has switched to GPL license.
The source code that have been released under BSD will be moved to a special tag on SVN, the trunk will be the newer GPLed source tree.
This project won't accept new developers until all the license policy is clearly defined.
(See discuss in Switch to GPL license?)
Patchs and bugs report are welcome.
Do we need two forms for each TA function?
There are 2 forms could be defined for each TA function, such as:</p>
1. Var emaVar(Var var, Opt period) 2. float ema(int idx, Var var, Opt period)
The first one will return a series of variable which have been calculated;<br /> The second one just return one value in the series that is exactly at point idx.
Now, let's apply the two forms to the MACD indicator:
public class MACDIndicator extends AbstractContIndicator { Opt periodS = new DefaultOpt("Period Short", 12.0); Opt periodL = new DefaultOpt("Period Long", 26.0); Opt periodD = new DefaultOpt("Period Diff", 9.0 ); Var<Float> dif, dea, macd, ema1, ema2; protected void go() { dif = new DefaultVar("DIF", Chart.LINE); // here, the emaVar may be wrongly used, because the dif // should be pre-calculated. dea = emaVar(dif, periodD); macd = new DefaultVar("MACD", Chart.STICK); ema1 = emaVar(C, periodS); ema2 = emaVar(C, periodL); } protected void computeCont(int begIdx) { for (int i = begIdx; i < _dataSize; i++) { dif.set(i, ema1.get(i) - ema2.get(i)); macd.set(i, dif.get(i) - dea.get(i)); } } }
In the MACD example, there came out calculating dependency order issue if we apply 1st form function no properly. That is, the 'dea' actually should be calculated after the 'dif'. And it's difficult to insert the emaVar() into the proper place (should be in the 'for' clause, exactly after the point 'i' of 'dif' is calculated), because emaVar() is not aware of 'i'
To avoid these confusion, we should only provide the second function:
float ema(int idx, Var var, Opt period)
and force the developer to write the code as:
public class MACDIndicator extends AbstractContIndicator { Opt periodS = new DefaultOpt("Period Short", 12.0); Opt periodL = new DefaultOpt("Period Long", 26.0); Opt periodD = new DefaultOpt("Period Diff", 9.0 ); Var<Float> dif, dea, macd, ema1, ema2; { _sname = "MACD"; _lname = "Moving Average Convergence/Divergence"; ema1 = new DefaultVar(); ema2 = new DefaultVar(); dif = new DefaultVar("DIF", Chart.LINE); dea = new DefaultVar("DEA", Chart.LINE); macd = new DefaultVar("MACD", Chart.STICK); } protected void computeCont(int begIdx) { for (int i = begIdx; i < _dataSize; i++) { ema1.set(i, ema(i, C, periodS)); ema2.set(i, ema(i, C, periodL)); dif.set(i, ema1.get(i) - ema2.get(i)); dea.set(i, ema(i, dif, periodD)); macd.set(i, dif.get(i) - dea.get(i)); } } }
This version makes the calculating dependency order clearly, no more confusion.</p>
Switch to GPL license?
I'm thinking to switch the license of BlogTrader Platform from BSD to GPL.
Making the license under GPL will encourage the contributors (include me) to release more source code to public, and means compatible to GPLed softwares that may be integrated into this project, such as: Sleepycat's Berkeley DB.
Of course, the source code that had been released will remain under BSD license, but I will not update it any more.
How to add a dropdown menu to toolbar in NetBeans Platform
To add a dropdown menu to toolbar, you should extends CallableSystemAction and override method: public Component getToolbarPresenter()
Dropdown menu should be banded with a JToggleButton, when JToggleButton is pressed down, show a JPopupMenu on JToggleButton.
Example:
public class PickDrawingLineAction extends CallableSystemAction { private static JToggleButton toggleButton; private static ButtonGroup buttonGroup; private static JPopupMenu popup; private MyMenuItemListener menuItemListener; List<AbstractHandledChart> handledCharts; public PickDrawingLineAction() { } public void performAction() { java.awt.EventQueue.invokeLater(new Runnable() { public void run() { toggleButton.setSelected(true); } }); } public String getName() { return "Pick Drawing Line"; } public HelpCtx getHelpCtx() { return HelpCtx.DEFAULT_HELP; } protected boolean asynchronous() { return false; } public Component getToolbarPresenter() { Image iconImage = Utilities.loadImage( "org/blogtrader/platform/core/netbeans/ resources/drawingLine.png"); ImageIcon icon = new ImageIcon(iconImage); toggleButton = new JToggleButton(); toggleButton.setIcon(icon); toggleButton.setToolTipText("Pick Drawing Line"); popup = new JPopupMenu(); menuItemListener = new MyMenuItemListener(); handledCharts = PersistenceManager.getDefalut() .getAllAvailableHandledChart(); buttonGroup = new ButtonGroup(); for (AbstractHandledChart handledChart : handledCharts) { JRadioButtonMenuItem item = new JRadioButtonMenuItem(handledChart.toString()); item.addActionListener(menuItemListener); buttonGroup.add(item); popup.add(item); } toggleButton.addItemListener(new ItemListener() { public void itemStateChanged(ItemEvent e) { int state = e.getStateChange(); if (state == ItemEvent.SELECTED) { // show popup menu on toggleButton at position:(0, height) popup.show(toggleButton, 0, toggleButton.getHeight()); } } }); popup.addPopupMenuListener(new PopupMenuListener() { public void popupMenuCanceled(PopupMenuEvent e) { toggleButton.setSelected(false); } public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { toggleButton.setSelected(false); } public void popupMenuWillBecomeVisible(PopupMenuEvent e) { } }); return toggleButton; } private class MyMenuItemListener implements ActionListener { public void actionPerformed(ActionEvent ev) { JMenuItem item = (JMenuItem)ev.getSource(); String selectedStr = item.getText(); AnalysisChartTopComponent analysisTc = AnalysisChartTopComponent.getSelected(); if (analysisTc == null) { return; } AbstractChartViewContainer viewContainer = analysisTc.getSelectedViewContainer(); AbstractChartView masterView = viewContainer.getMasterView(); if (!(masterView instanceof WithDrawingPart)) { return; } DrawingPart drawingPart = ((WithDrawingPart)masterView).getCurrentDrawing(); if (drawingPart == null) { JOptionPane.showMessageDialog( WindowManager.getDefault().getMainWindow(), "Please add a layer firstly to pick line type", "Pick line type", JOptionPane.OK_OPTION, null); return; } AbstractHandledChart selectedHandledChart = null; for (AbstractHandledChart handledChart : handledCharts) { if (handledChart.toString().equalsIgnoreCase(selectedStr)) { selectedHandledChart = handledChart; break; } } if (selectedHandledChart == null) { return; } AbstractHandledChart handledChart = selectedHandledChart.createNewInstance(); handledChart.setPart(drawingPart); drawingPart.setHandledChart(handledChart); Series masterSeries = viewContainer.getMasterSeries(); DrawingDescriptor description = viewContainer.getDescriptors().findDrawingDescriptor( drawingPart.getLayerName(), masterSeries.getUnit(), masterSeries.getNUnits()); if (description != null) { Node stockNode = analysisTc.getActivatedNodes()[0]; Node node = stockNode.getChildren() .findChild(DescriptorGroupNode.DRAWINGS) .getChildren().findChild(description.getDisplayName()); if (node != null) { ViewAction action = (ViewAction)node.getLookup().lookup(ViewAction.class); assert action != null : "view action of this layer's node is null!"; action.view(); } } else { /** best effort, should not happen */ viewContainer.setCursorCrossVisible(false); drawingPart.setActived(true); SwitchHideShowDrawingLineAction.updateToolbar(viewContainer); } } } }
How to hide/show toolbar dynamically in NetBeans Platform
To hide/show toolbar dynamically in NetBeans Platform, you should pre-define a toolbar Configuration firstly, then set to what you want via:
ToolbarPool.getDefault().set(String toolbarConfiguratonName);
1. Define toolbar configuration files in module's resource director: blogtrader-platform-branding\src\org\blogtrader\platform\branding\netbeans\resources\Toolbars\
Standard.xml:
<?xml version="1.0"?> <!DOCTYPE Configuration PUBLIC "-//NetBeans IDE//DTD toolbar//EN" "http://www.netbeans.org/dtds/toolbar.dtd"> <Configuration> <Row> <Toolbar name="View" /> <Toolbar name="Control" /> <Toolbar name="Indicator" /> <Toolbar name="Draw" /> <Toolbar name="Memory" /> </Row> <Row> <Toolbar name="File" position="2" visible="false" /> <Toolbar name="Edit" position="2" visible="false" /> <Toolbar name="Build" position="2" visible="false" /> <Toolbar name="Debug" position="2" visible="false" /> <Toolbar name="Versioning" position="2" visible="false" /> </Row> </Configuration>
Developing.xml:
<?xml version="1.0"?> <!DOCTYPE Configuration PUBLIC "-//NetBeans IDE//DTD toolbar//EN" "http://www.netbeans.org/dtds/toolbar.dtd"> <Configuration> <Row> <Toolbar name="View" /> <Toolbar name="Control" /> <Toolbar name="Indicator" /> <Toolbar name="Draw" /> <Toolbar name="Memory" /> </Row> <Row> <Toolbar name="File" position="2" /> <Toolbar name="Edit" position="2" /> <Toolbar name="Build" position="2" /> <Toolbar name="Debug" position="2" visible="false" /> <Toolbar name="Versioning" position="2" visible="false" /> </Row> </Configuration>
2. register the configuration files in layer.xml:<br>
<?xml version="1.0"?> <!DOCTYPE filesystem PUBLIC "-//NetBeans//DTD Filesystem 1.0//EN" "http://www.netbeans.org/dtds/filesystem-1_0.dtd"> <filesystem> <folder name="Toolbars"> <file name="Standard.xml" url="Toolbars/Standard.xml"> </file> <attr name="Standard.xml/Developing.xml" boolvalue="true" /> <file name="Developing.xml" url="Toolbars/Developing.xml"> </file> </folder > </filesystem>
3. set to toolbar configuration that you want via:
// change to toolbar layer as Standard ToolbarPool.getDefault().set("Standard"); // change to toolbar layer as Developing ToolbarPool.getDefault().set("Developing");
The first custom indicator lives in BlogTrader Platform
Here's the screenshot shows the first custom indicator lives in the coming BlogTrader? Platform which is scheduled at mid of May.
An Elliott Wave analyse on Shanghai Index 4 years ago
It was 4 years ago, I wrote an article about the Elliott Wave analyse on Shanghai Stock Exchange Index. At that time, the Shanghai Index dropped from the highest point 2245 at Jun 2001 to a low around 1300. I posted a picture and made some estimations:
* Be ware of that a Big C wave, which may point to around 1043 * The C wave may last to at least 65x3=195 weeks (about to June 2005)
4 years passed, the Shanghai Index arrived a lowest 998 at June 2005. I think that a new wave-1 has been in its step, and the 2007 will be a good year.
Here's the link to orginal post (in Chinese) and the picture:
Programming language for custom indicator?
I tried to write the MA indicator in javascript, here's what it looks:
this._sname = "MA"; this._lname = "Moving Average"; this._overlapping = true; var period1 = new P("Period 1", 5.0 ); var period2 = new P("Period 2", 10.0); var period3 = new P("Period 3", 20.0); var ma1 = new Var("ma1", Chart.LINE); var ma2 = new Var("ma2", Chart.LINE); var ma3 = new Var("ma3", Chart.LINE); function computeCont(int fromIdx) { for (int i = fromIdx; i < this._dataSize; i++) { ma1.set(i, ma(i, C, period1.value())); ma2.set(i, ma(i, C, period2.value())); ma3.set(i, ma(