Yhc/Javascript/Programmers guide/Echo demo source

From HaskellWiki
< Yhc‎ | Javascript‎ | Programmers guide
Revision as of 03:38, 21 November 2006 by DimitryGolubovsky (talk | contribs) (Colored source of the Echo demo program)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.

module Echo where -- This program demonstrates the lowest possible level of interaction -- with Javascript. No extra libraries, no programming paradigms: -- just some helper functions defined directly via unsafeJS. -- Import the UnsafeJS module to get access to the low-level Javascript, -- that is, ability to wrap arbitrary Javascript expressions into -- a Haskell-callable function. See a special section of ycr2js -- Programmers guide where usage of unsafeJS is discussed in details. import UnsafeJS import Char -- Import this module for decimal to Roman and Roman to decimal conversions. -- This also serves as a primitive benchmark, and shows that third-party code -- may also be brought into web browser. import Roman -- Import this module to be able to define a monad which provides -- correct ordering of Javascript execution. import Control.Monad -- Opaque type for Javascript objects. No values of this type are constructed -- in the Haskell code. But every object returned from Javascript belongs to -- this type. newtype JSObject = JSObject () -- A simple monad used for enforcing actions order only. -- Both its bind operations (>>=, >>) should evaluate their -- left-hand-side expression to make sure all Javascript computations -- associated to that expression have been completed before execution -- goes to the right-hand-side expression. -- Even >> should evaluate its LHS argument although it is "lost" -- for the RHS expression. This is achieved via pattern-matching. -- The same holds for return. data JS a = JS a instance Monad JS where -- bind: execute the LHS expression, extract result from the monad, -- pass on to the RHS expression. (JS a) >>= fn = fn a -- anomymous bind: execute the LHS expression, then the RHS expression -- that is, enforce correct execution order only. (JS a) >> fn = fn -- The return function must evaluate its argument before wrapping it into -- monad. This is important e. g. for exception handlers, otherwise -- nothing may be evaluated within exception handler, and actual exception -- will occur outside of the exception handler and will not be caught properly. return a = a `seq` (JS a) -- Here are the functions to access web-browser's DOM structures. -- In the absence of the standardized framework, these functions -- are coded on ad-hoc basis, and their interface may be imperfect, -- but this is a good demonstration what kind of stuff may be needed -- for a real framework. -- Get the document interface reference getDocument :: JS JSObject -- This (and other) examples show how to return data (including monadic) values -- from Javascript to Haskell. The HSData constructor finction is used allover -- the Javascript code generated form Haskell source to create data objects. -- Its first argument is data constructor name index, and its second argument -- is an array of data object member values in proper order. To obtain -- data constructor name index, one has to refer to the conIdx global variable -- which is indexed by constructor qualified name. Since the JS monad is defined -- in this module named Echo, qualified name of the constructor is Echo.JS. -- Web browser DOM interface defines a global name for the document interface -- access, that is, `document'. Its value is wrapped into the JS monad -- and returned. getDocument = unsafeJS "return new HSData(conIdx['Echo.JS'],[document]);" -- Obtain a property of an object getProperty :: JSObject -> String -> JS JSObject -- Similarly, return a value wrapped into the IO monad. The `a' argument -- is the reference to a Javascript object, and the `b' argument is -- a string containing name of the property to be retrieved. The function -- evaluates both of its arguments and retrieves the property name of `a' -- as an associative array element using the value of `b'. getProperty a b = unsafeJS "return new HSData(conIdx['Echo.JS'],[exprEval(a)[exprEval(b).toString()]]);" -- Modify a property of an object setProperty :: JSObject -> String -> a -> JS () -- Set property named `b' of the object refered to as `a' to the value of `c'. -- The third argument may be of any type, but its evaluation should terminate. -- As a side note: this function returns a `void' value (unit, ()). Its type signature -- might be rearranged, like this: -- -- String -> a -> JSObject -> JS JSObject -- that is, returns the same object with -- -- property modified -- -- then operations of setting object property might be chained in the JS monad: -- -- obj' <- setProperty "prop1" val1 obj >>= -- setProperty "prop2" val2 >>= -- setProperty "prop3" val3 setProperty a b c = unsafeJS "exprEval(a)[exprEval(b).toString()]=exprEval(c);return new HSData(conIdx['Echo.JS'],[]);" -- Invoke a method of an object runMethod :: JSObject -> String -> a -> JS JSObject -- This function obtains reference to an object's method similarly -- to obtaining reference to a property, using the method name string. -- Its third argument `c' must be a list. It is converted into an array -- of function arguments and the method is applied to this array. -- This function uses `cbrApply' rather than the Javascript function object's -- `apply' method for cross-browser compatibility: MSIE DOM functions do not have -- the `apply' method, so cbrApply detects that and works around this issue. -- Evaluation of expressions in `c' is done by the `toArray' method which is properly -- overloaded for Haskell lists as they are unmarshalled to Javascript. runMethod a b c = unsafeJS "var a1=exprEval(a); return new HSData(conIdx['Echo.JS'],[cbrApply(a1[exprEval(b).toString()],a1,c._toArray())]);" -- Output string representation of an object into the status line putStatLn :: a -> JS () -- This is a helper function which sets the `window.status' global object to -- the stringified value of the function's argument. Note how a unit value () -- is returned: an empty array is used as the second argument to HSData. While -- to be exact, one should retrieve an index of the () data constructor and -- construct another data object, the practice shown is acceptable in most cases, -- except for the situation when pattern matching is done on the unit value. putStatLn a = unsafeJS "window.status=exprEval(a).toString(); return new HSData(conIdx['Echo.JS'],[]);" -- Output source representation of an object into the status line putStatLnSrc :: a -> JS () -- Similarly to the function above, but `toSource' method is used. putStatLnSrc a = unsafeJS "window.status=exprEval(a).toSource(); return new HSData('Echo_46JS',[]);" -- Register an event handler (old DOM0 style) regEventHandler :: JSObject -> String -> (JSObject -> JS ()) -> JS () -- This function attaches an old-style event handler `c' to a DOM object -- (such as an input field) `a'. Name of the event is passed in `b' as string -- (e. g. "onkeypress"). This function works around browser incompatibility. -- Mozilla/Netscape/FireFox pass event information as an event handler's -- argument. MSIE passes it in a global object named `event'. This function -- wrap the Haskell handler function into an anonymous function which checks -- for its argument, and if unavailable, tries to get the information -- from the global `event' object. regEventHandler a b c = unsafeJS "exprEval(a)[exprEval(b).toString()]=function(e){if(!e){e=window.event;}; return exprEval(exprEval(c)._ap([e]));}; return new HSData(conIdx['Echo.JS'],[]);" -- Get the numeric representation of an object asInt :: JSObject -> JS Int -- This function (ab)uses the untypedness of Javascript and coerces -- any object to a numeric value. asInt a = unsafeJS "return new HSData(conIdx['Echo.JS'],[new Number(exprEval(a))]);" -- Get the String representation of an object asString :: JSObject -> JS String -- This function (ab)uses the untypedness of Javascript and coerces -- any object to a stringified value. asString a = unsafeJS "return new HSData(conIdx['Echo.JS'],[new String(exprEval(a))]);" -- Catch exceptions using Javascript exception machinery catchJS :: JS a -> (JSObject -> JS a) -> JS a -- This function installs an exception handler `b' while executing an expression -- in `a'. If an exception occurs, it will be passed to the handler which should -- return a "replacement" value. catchJS a b = unsafeJS "try {return exprEval(a);} catch(_e) {return exprEval(b)._ap([_e]);}" -- Get current time in ms since 1970 (for performance measurements) getTime :: Int -> JS Int -- This function obtains current time from the browser. The first argument -- is not used but is necessary in order to have this function evaluated -- more than once. In its absence, evaluation result will be stored in the -- getTime's function descriptor, and it will always return time of the first -- evaluation. getTime a = unsafeJS "return new HSData(conIdx['Echo.JS'],[(new Date()).getTime()]);" -- Key press handler -- element event void inkey :: JSObject -> JSObject -> JS () -- An event handler function has its first argument -- to pass the reference to the object which generates an event. Thus, -- although this builds a circular structure with a DOM object involved (may be harmful -- for MSIE garbage collection), this works around some incompatibilities -- betwenn browsers regarding how event source is encoded in the event. -- As an alternative, object "id" property value might be passed instead of -- reference to an object itself. This would avoid creation of a circular link, -- but would make execution of the event handler slightly longer. -- As a side note: Event handler's type signature might be modified to return -- a modified event. Using this approach, event handlers might be chained -- using the monadic `bind' function which passes output from its LHS expression -- to its RHS expression. -- This handler analyzes code of the key pressed. -- If it is Enter (13) then the input value is placed into a -- dynamically created div and inserted above the input field. inkey o e = do -- Obtain numeric code of the key pressed kcs <- getProperty e "keyCode" >>= asInt -- If `Enter' was pressed when (kcs == (13::Int)) $ do -- Obtain the time when `Enter' was pressed t1 <- getTime 0 -- Get the document interface doc <- getDocument -- Get the document body element interface body <- getProperty doc "body" -- Create a div dynamically mydiv <- runMethod doc "createElement" ["div"] -- Obtain the string typed: it is the value of the input element v <- getProperty o "value" >>= asString -- If the string was not empty when (length v > 0) $ do -- Conversion functions of the Roman module raise error -- when conversion cannot be done. In this case, empty string -- should be returned. rom <- catchJS (return $ (show . fromRoman) v) (\_ -> return "") dec <- catchJS (return $ toRoman (read v)) (\_ -> return "") -- Obtain the time after possible conversion is done t2 <- getTime 0 -- Format the result let dt = t2 - t1 let vr = v ++ " " ++ rom ++ " " ++ dec ++ " " ++ show dt ++ " ms" -- Output the result into the div element setProperty mydiv "innerHTML" vr -- Make the div visible right above the input element runMethod body "insertBefore" [mydiv, o] -- Clear the input element setProperty o "value" "" -- Return from the handler return () return () return () -- Main program. As in "traditional" Haskell programming, it has a monadic type. -- But since there is no "traditional" input/output in a web browser, the IO monad -- is useless here; use the JS monad. main :: JS () main = do -- Get the document interface doc <- getDocument -- Get the document body element interface body <- getProperty doc "body" -- Create the input field. inp <- runMethod doc "createElement" ["input"] -- Set the "id" property. It is unused in this demo, but -- might be helpful in general. setProperty inp "id" "input-echo" -- Append the input element ot the list of body's children -- thus visualizing it. runMethod body "appendChild" [inp] -- Register an event handler on the input field. Note that -- (inkey inp) is passed as an event handler; thus, its closure -- will contain reference to the input element that generates an event. regEventHandler inp "onkeypress" (inkey inp) -- Set input focus. runMethod inp "focus" [] return ()