Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Ur/Web: lightning fast prototyping for the web

Ur/Web: lightning fast prototyping for the web

F-café, April 2018
PyCon Ukraine, April 2018, updated slides

Demo at https://github.com/proger/urc

Volodymyr Kyrylov

April 13, 2018
Tweet

More Decks by Volodymyr Kyrylov

Other Decks in Programming

Transcript

  1. protobuf.load("awesome.proto", function(err, root) { if (err) throw err; // Obtain

    a message type var AwesomeMessage = root.lookupType("awesomepackage.AwesomeMessage"); // Exemplary payload var payload = { awesomeField: "AwesomeString" }; // Verify the payload if necessary (i.e. when possibly incomplete or invalid) var errMsg = AwesomeMessage.verify(payload); if (errMsg) throw Error(errMsg); // Create a new message var message = AwesomeMessage.create(payload); // or use .fromObject if conversion is necessary // Encode a message to an Uint8Array (browser) or Buffer (node) var buffer = AwesomeMessage.encode(message).finish(); // ... do something with buffer // Decode an Uint8Array (browser) or Buffer (node) to a message var message = AwesomeMessage.decode(buffer); // ... do something with message // If the application uses length-delimited buffers, there is also encodeDelimited and decodeDelimited. // Maybe convert the message back to a plain object var object = AwesomeMessage.toObject(message, { longs: String, enums: String, bytes: String, // see ConversionOptions }); });
  2. protobuf.load("awesome.proto", function(err, root) { if (err) throw err; // Obtain

    a message type var AwesomeMessage = root.lookupType("awesomepackage.AwesomeMessage"); // Exemplary payload var payload = { awesomeField: "AwesomeString" }; // Verify the payload if necessary (i.e. when possibly incomplete or invalid) var errMsg = AwesomeMessage.verify(payload); if (errMsg) throw Error(errMsg); // Create a new message var message = AwesomeMessage.create(payload); // or use .fromObject if conversion is necessary // Encode a message to an Uint8Array (browser) or Buffer (node) var buffer = AwesomeMessage.encode(message).finish(); // ... do something with buffer // Decode an Uint8Array (browser) or Buffer (node) to a message var message = AwesomeMessage.decode(buffer); // ... do something with message // If the application uses length-delimited buffers, there is also encodeDelimited and decodeDelimited. // Maybe convert the message back to a plain object var object = AwesomeMessage.toObject(message, { longs: String, enums: String, bytes: String, // see ConversionOptions }); }); Want to avoid encoding/decoding values all over the place google/protobuf
  3. {-# LANGUAGE DeriveGeneric, DeriveAnyClass #-} module Db where import Elm

    import GHC.Generics data Person = Person { id :: Int , name :: Maybe String } deriving (Show, Eq, Generic, ElmType) import Data.Proxy import Db import Elm spec :: Spec spec = Spec ["Db", "Types"] [ "import Json.Decode exposing (..)" , "import Json.Decode.Pipeline exposing (..)" , toElmTypeSource (Proxy :: Proxy Person) , toElmDecoderSource (Proxy :: Proxy Person) ] main :: IO () main = specsToDir [spec] "some/where/output" Or write typing glue every time krisajenkins/elm-export
  4. <body> <textarea id="chat-log" cols="100" rows="20"></textarea><br/> <input id="chat-message-input" type="text" size="100"/><br/> <input

    id="chat-message-submit" type="button" value="Send"/> </body> <script> var roomName = {{ room_name_json }}; var chatSocket = new WebSocket( 'ws://' + window.location.host + '/ws/chat/' + roomName + '/'); chatSocket.onmessage = function(e) { var data = JSON.parse(e.data); var message = data['message']; document.querySelector('#chat-log').value += (message + '\n'); }; chatSocket.onclose = function(e) { console.error('Chat socket closed unexpectedly'); }; </script>
  5. var roomName = {{ room_name_json }}; var chatSocket = new

    WebSocket( 'ws://' + window.location.host + '/ws/chat/' + roomName + '/'); chatSocket.onmessage = function(e) { var data = JSON.parse(e.data); document.querySelector( '#chat-log' ).value += (data['message'] + '\n'); }; chatSocket.onclose = function(e) { console.error('oops'); };
  6. <body> <textarea id="chat-log" cols="100" rows="20"></textarea><br/> <input id="chat-message-input" type="text" size="100"/><br/> <input

    id="chat-message-submit" type="button" value="Send"/> </body> <script> var roomName = {{ room_name_json }}; var chatSocket = new WebSocket( 'ws://' + window.location.host + '/ws/chat/' + roomName + '/'); chatSocket.onmessage = function(e) { var data = JSON.parse(e.data); var message = data['message']; document.querySelector('#chat-log').value += (message + '\n'); }; chatSocket.onclose = function(e) { console.error('Chat socket closed unexpectedly'); }; </script> Stuff you hate doing in a web app: serialization, APIs and URLs
  7. Not Contenders Elm / PureScript / Fable Python in the

    Browser Everything that "shares code"
  8. The Language Almost like SML Fun with declaration ordering and

    mutual recursion ML modules and functors “header” files Compiler generates JS for you as well C and JS FFI, SQL and XML is sugar No REPL :D
  9. fun id [a] (x : a) : a = x

    fun compose [a] [b] [c] (f : b -> c) (g : a -> b) (x : a) : c = f (g x) “-XUndecidableInstances”: type inference is undecidable by default so watch out
  10. fun mp [a] [b] (f : a -> b) :

    list a -> list b = let fun loop (ls : list a) = case ls of [] => [] | x :: ls' => f x :: loop ls' in loop end
  11. datatype tree a = Leaf of a | Node of

    tree a * tree a fun size [a] (t : tree a) : int = case t of Leaf _ => 1 | Node (t1, t2) => size t1 + size t2
  12. signature S = sig val client_id : string val client_secret

    : string val https : bool val onLogin : profile -> transaction page end Module signatures
  13. functor Make(M : S) : sig val authorize : transaction

    page val whoami : transaction (option string) val loadProfile : string -> transaction profile val logout : transaction unit end Functors: parametrized modules
  14. val x = { A = 0, B = True

    } x.A type somerec = { A : int, B : bool } fun getA (r : somerec) = r.A getA (x -- #A ++ {A = 4}) Row types
  15. Web

  16. #war-room bot: jenkins is down dev: phew, no more tyre

    fires dev: can finally have a weekend off dev: make sure nobody brings it back up ¯\_(ツ)_/¯
  17. fun main () : transaction page = {User = user,

    Header = hdr} <- userHeader; textId <- fresh; text <- source ""; init <- prefetch (); ch <- subscribe; rest <- source <xml/>; received <- source []; let (* ... *) in return (Def.page "Urc" hdr (onload ()) <xml> <section> <h2>chatroom</h2> <div> <ctextbox id={textId} source={text} /> <button value="Send" onclick={fn _ => submit ()} /><br/> </div> </section> <section> <dyn signal={signal rest} /> {init} </section> </xml>) end transaction generates both client and server code
  18. fun main () : transaction page = {User = user,

    Header = hdr} <- userHeader; textId <- fresh; text <- source ""; init <- prefetch (); ch <- subscribe; rest <- source <xml/>; received <- source []; let (* ... *) in return (Def.page "Urc" hdr (onload ()) <xml> <section> <h2>chatroom</h2> <div> <ctextbox id={textId} source={text} /> <button value="Send" onclick={fn _ => submit ()} /><br/> </div> </section> <section> <dyn signal={signal rest} /> {init} </section> </xml>) end
  19. fun listen () = s <- recv ch; xs <-

    get received; set received (s :: xs); set rest (List.foldr join <xml/> (List.mp format (s :: xs))); listen () and onload () = _ <- reset (); listen () and submit () : transaction unit = message <- get text; time <- now; rpc (publish' {Author = user, Timestamp = time, Message = message}); _ <- reset (); return ()
  20. transaction generates both client and server code No POST, No

    GET No URLs No HTTP status codes (200 or 500) Client-to-server (rpc) Server-to-client (channel, signal + source + dyn)
  21. % urweb -dumpSource -timing test parseJob: 3.14E~4 parse: 1.87E~4 elaborate:

    0.601906 unnest: 0.002338 explify: 0.002041 corify: 0.002383 core_untangle: 2.81E~4 shake1: 2.23E~4 especialize1': 1.8E~5 shake1': 3E~6 rpcify: 1.9E~5 core_untangle2: 0 shake2: 2E~6 especialize1: 1E~6 core_untangle3: 0 shake3: 1E~6 tag: 3.3E~5 reduce: 4.4E~5 shakey: 1E~6 unpoly: 1.7E~5 specialize: 1.7E~5 shake4: 1E~6 especialize2: 1E~6 shake4': 1E~6 unpoly2: 1E~6 specialize2: 0 shake4': 0
  22. val spinner : xbody = <xml> <div class="sk_cube-grid"> <div class="sk_cube

    sk_cube1"></div> <div class="sk_cube sk_cube2"></div> <div class="sk_cube sk_cube3"></div> <div class="sk_cube sk_cube4"></div> <div class="sk_cube sk_cube5"></div> <div class="sk_cube sk_cube6"></div> <div class="sk_cube sk_cube7"></div> <div class="sk_cube sk_cube8"></div> <div class="sk_cube sk_cube9"></div> </div> </xml>
  23. ok <- oneRowE1 (SELECT COUNT( * ) > 0 FROM

    secrets WHERE secrets.Login = {[r.Login]} AND secrets.Secret = {[r.Secret]});
  24. % urweb -dumpSource -dumpTypes -stop tag test <Test> </Test> con

    int :: Type = FFI(Basis.int) con bool :: Type = FFI(Basis.bool) table users as Test_users : [#Bobby = bool, #Id = int] keys FFI(Basis.no_primary_key) [[#Bobby = bool, #Id = int]] constraints FFI(Basis.no_constraint) [[#Bobby = bool, #Id = int]]
  25. structure Admin = Crud.Make(struct con key = #Login val tab

    = GH.users val title = "Authenticated Users" val everything = { Login = Crud.string "Login", AvatarUrl = Crud.string "Avatar URL", Nam = Crud.option_string "Name", (* ... *) } type task_type = {} val runTask = fn _ username => redirect (bless ("https://github.com/" ^ username)) val taskTitle = fn _ => "[Visit]" val allTasks = () :: [] val header = { User = _, Header = hdr } <- userHeader; return hdr end)
  26. FFI

  27. uw_Basis_string uw_WorldFfi_post(uw_context ctx, uw_Basis_string url, uw_Basis_string body) { CURL *c

    = curl(ctx); curl_easy_reset(c); curl_easy_setopt(c, CURLOPT_POSTFIELDS, body); uw_Basis_string ret = doweb(ctx, c, url, 1); return ret; }
  28. val post : url -> string -> transaction string val

    get : url -> option string -> transaction string
  29. { stdenv, fetchgit, autoreconfHook, curl, urweb }: stdenv.mkDerivation { name

    = "urweb-curl"; buildInputs = [ autoreconfHook urweb ]; propagatedBuildInputs = [ curl ]; preConfigure = '' export CFLAGS="-I${urweb}/include/urweb" ''; src = fetchgit { inherit (builtins.fromJSON (builtins.readFile ./urweb-curl.json) ) url rev sha256; }; }