Comparing Dynamic and Static Language Approaches to Web Frameworks Neil Brown School of Computing University of Kent UK 30 April 2012
2012-05-01 Comparing Dynamic and Static Language Approaches to Web Frameworks Comparing Dynamic and Static Language Approaches to Web Frameworks Neil Brown School of Computing University of Kent UK 30 April 2012 Slides with blue headers are original slides, these grey pages are notes pages. I should point that out that my opinion in this talk is biased: I ve been using Rails for a few years, and suffering with maintaining our Rails sites. I believe in static typing, powerful compilers, and making as many errors as possible into compile-time errors (hence I like Haskell). So I was interested to try Yesod and see how far it was able to use these static features, compared to Rails, for the same end result. That led to this talk. I m partly comparing Yesod to Rails, but I m more interested in comparing the use of Haskell s static approach (and Template Haskell meta-programming) to Ruby s dynamic approach, and Yesod and Rails provide good exemplars for this.
Web Frameworks Web Frameworks
2012-05-01 Comparing Dynamic and Static Language Approaches to Web Frameworks Web Frameworks Web Frameworks Web Frameworks Many websites are ultimately ways to read and write to a database. Web frameworks offer support for common website designs (as in structure/software, not visuals) They are vaguely MVC, with: models stored in database, read by views, written to by forms, with all the wiring to URLs accomplished by routing.
Web Frameworks Web Frameworks
Web Frameworks: Yesod vs Rails Web Frameworks Yesod (Haskell) I m using 1.0 (most recent) Ruby on Rails (Ruby) I m using 2.3 (not most recent)
2012-05-01 Comparing Dynamic and Static Language Approaches to Web Frameworks Web Frameworks Web Frameworks: Yesod vs Rails Web Frameworks: Yesod vs Rails Yesod (Haskell) Ruby on Rails (Ruby) I m using 1.0 (most recent) I m using 2.3 (not most recent) Ruby is an OOP language with dynamic typing (lots of duck typing). Haskell is a functional language with static typing. Rails uses a lot of dynamic features, Yesod uses a lot of static features, so they are at opposite ends of the scale, yet accomplishing almost exactly the same thing. I gather Rails 3.0 has many changes over 2.3, but we have not yet migrated our sites. I don t think many changes alter this presentation much.
Web Frameworks Software Engineering Challenges To make life easier, need lots of boilerplate helper functions, e.g. username_for_id(int user_id), show_user_url(int user_id) Formulaic, yet specific to each project Best to auto-generate using meta-programming...
2012-05-01 Comparing Dynamic and Static Language Approaches to Web Frameworks Web Frameworks Software Engineering Challenges Software Engineering Challenges To make life easier, need lots of boilerplate helper functions, e.g. username_for_id(int user_id), show_user_url(int user_id) Formulaic, yet specific to each project Best to auto-generate using meta-programming... The following examples of meta-programming in Ruby and Haskell are not what you would see when you use Rails or Yesod, but I wanted to give a quick idea of the key points of what is happening underneath.
Low-Level Metaprogramming: Ruby Web Frameworks Add new methods at run-time using blocks, something like: class Example def add_method self. class.send(:define_method, :get_const) do return 6 end end end Body is a standard block of code (like closure), added at run-time.
2012-05-01 Comparing Dynamic and Static Language Approaches to Web Frameworks Web Frameworks Low-Level Metaprogramming: Ruby Low-Level Metaprogramming: Ruby Add new methods at run-time using blocks, something like: class Example def add_method self.class.send(:define_method, :get_const) do return 6 end end end Body is a standard block of code (like closure), added at run-time. The :blah syntax is a symbol, which is like an interned string. The method is added to the Example class at run-time, when you execute add method This dynamic method addition is precisely what makes so many errors run-time errors in Ruby: you can t tell if you re calling a non-existent method at compile -time, because that method might get added later on. It also complicates code completion and so on.
Web Frameworks Low-Level Metaprogramming: Haskell Add source code at compile-time, using Template Haskell functions that return Haskell AST: $(return <$> fund (mkname "getconst") [return $ Clause [] (NormalB $ LitE $ IntegerL 6) []]) Normally use higher-level functions! Run during compilation, then outputted AST is compiled.
2012-05-01 Comparing Dynamic and Static Language Approaches to Web Frameworks Web Frameworks Low-Level Metaprogramming: Haskell Low-Level Metaprogramming: Haskell Add source code at compile-time, using Template Haskell functions that return Haskell AST: $(return <$> fund (mkname "getconst") [return $ Clause [] (NormalB $ LitE $ IntegerL 6) []]) Normally use higher-level functions! Run during compilation, then outputted AST is compiled. As a Yesod user, you never see any of this! But the point is, the metaprogramming generates a piece of abstract syntax tree (AST) which is then compiled. So the Haskell compiler effectively has several phases: 1. Run any Template Haskell code, between $(blah) (or the quasi-quote notation, which I don t happen to use in this presentation) 2. This Template Haskell produces Haskell AST as its output, which is spliced into that location in the file, as if it had always been there. 3. Compile the complete source file). You can think of Template Haskell as a preprocessor, like the C preprocessor, but it uses full Haskell as its language, produces AST instead of text, and I believe has access to information from the compiler.
Web Frameworks Models
Models Models A model is an entity like user, blog post, comment, etc. Both frameworks offer persistence for models.
Models Models A model is an entity like user, blog post, comment, etc. Both frameworks offer persistence for models. The model persistence usually uses an RDBMS. However, the models are not a relational database.
Rails Models
Models in Rails Models create_table : blog_posts do t t. text : title t. text :content t. integer :author_id end create_table :authors do t t. string :name end
2012-05-01 Comparing Dynamic and Static Language Approaches to Web Frameworks Models Models in Rails Models in Rails create_table :blog_posts do t t.text : title t. text :content t. integer :author_id end create_table :authors do t t. string :name end This is part of a migration, rather than a model definition as such, but this is how you define new models in Rails. I ve chosen not to cover migrations in this talk, because they are not that interesting in terms of the dynamic/static language divide.
Models in Rails Models class Author < ActiveRecord::Base validates_uniqueness_of :name has_many :blog_posts # implicit public field : name, blog_posts # inherited methods: new, create, save end class BlogPost < ActiveRecord::Base belongs_to :author # implicit public fields : author, title, content # inherited methods: new, create, save end
2012-05-01 Comparing Dynamic and Static Language Approaches to Web Frameworks Models Models in Rails Models in Rails class Author < ActiveRecord::Base validates_uniqueness_of :name has_many :blog_posts # implicit public field : name, blog_posts # inherited methods: new, create, save end class BlogPost < ActiveRecord::Base belongs_to :author # implicit public fields : author, title, content # inherited methods: new, create, save end The fields that the migration added are then inserted (somehow!) into the classes by Rails. The save (etc) methods store these fields of the model into the database and handle the associations (links to other models) automatically. Note that the classes and migrations and tables in the RDBMS and matched by name: the class Author is stored in the table authors, and BlogPost in blog posts, with Rails handling the conversion from camel-case to underscore. Also, the blog post to author link automatically picks up author id as the reference.
Yesod Models
Models in Yesod Models The models are written in their own file: Author name Text UniqueAuthor name BlogPost author AuthorId title Text content Text
2012-05-01 Comparing Dynamic and Static Language Approaches to Web Frameworks Models Models in Yesod Models in Yesod The models are written in their own file: Author name Text UniqueAuthor name BlogPost author AuthorId title Text content Text This model file (much like the routes file later on) is not Haskell code (whereas Rails migrations are Ruby code), but instead is a simple Domain-Specific Language (DSL) for expressing models. This DSL is parsed by some Template Haskell, and turned into code for the data types, and for handling the persistence.
Models in Yesod Models Generated code from the models file: data Author = Author {authorname :: Text, authorpassword :: Maybe Text} type AuthorId = Key Author data BlogPost = BlogPost {blogpostauthor :: AuthorId, blogposttitle :: Text, blogpostcontent :: Text} type BlogPostId = Key BlogPost
2012-05-01 Comparing Dynamic and Static Language Approaches to Web Frameworks Models Models in Yesod Models in Yesod Generated code from the models file: data Author = Author {authorname :: Text, authorpassword :: Maybe Text} type AuthorId = Key Author data BlogPost = BlogPost {blogpostauthor :: AuthorId, blogposttitle :: Text, blogpostcontent :: Text} type BlogPostId = Key BlogPost I ve simplified the types slightly, because the generated types (and keys) are also parameterised by the persistence backend. However, that s a bit too much detail for this talk, and the extra type parameter doesn t alter much during your use of Yesod.
Fetching Model in Rails Models blog_post = BlogPost.find_by_id(blog_post_id) author = blog_post.author Implicit database access (and association load)
Fetching Model in Yesod Models rundb $ do blogpost <- get blogpostid author <- get (blogpostauthor blogpost) Explicit database access (and association load)
2012-05-01 Comparing Dynamic and Static Language Approaches to Web Frameworks Models Fetching Model in Yesod Fetching Model in Yesod rundb $ do blogpost <- get blogpostid author <- get (blogpostauthor blogpost) Explicit database access (and association load) Having the database transaction in its own monad makes it a bit clearer what will happen on rollback (because there can t have been any IO side-effects).
Models Fetching Model in Yesod rundb $ do blogpost <- get blogpostid author <- get (blogpostauthor blogpost) Explicit database access (and association load) Polymorphic get!
Models Models DB access more explicit (and more fiddly) in Yesod Wrong field error: Rails run-time, Yesod compile-time Each ID has distinct type in Yesod: avoids errors, allows conciseness
Association Helper Function Models Type-safe, no reflection: getand :: Key a -> (a -> Key b) -> DB (a, b) getand parentkey subkeyfunc = do parent <- get parentkey sub <- get (subkeyfunc parent) return (parent, sub) getand blogpostid blogpostauthor
2012-05-01 Comparing Dynamic and Static Language Approaches to Web Frameworks Models Association Helper Function Association Helper Function Type-safe, no reflection: getand :: Key a -> (a -> Key b) -> DB (a, b) getand parentkey subkeyfunc = do parent <- get parentkey sub <- get (subkeyfunc parent) return (parent, sub) getand blogpostid blogpostauthor This is an example of defining a function that works for any persistent type, without using reflection, etc. This means that you can write functions that are closer to Rails association loading, if you want.
Web Frameworks Routing
Routing Routing Routing is about mapping URLs to actions, and actions to URLs For example:
Routing Routing Routing is about mapping URLs to actions, and actions to URLs For example: http://www.example.com/blog_posts/1 displays blog post with ID=1
Routing Routing Routing is about mapping URLs to actions, and actions to URLs For example: http://www.example.com/blog_posts/1 displays blog post with ID=1 blog_post(1) helper function returns http://www.example.com/blog_posts/1
Rails Routing
Routing in Rails Routing map.resources :blog_posts roughly short for: map.resources :blog_posts, :member => {:show => :get, :edit => :get, :update => :put}, : collection => {:index => :get, :new => :get, :create => :post}
2012-05-01 Comparing Dynamic and Static Language Approaches to Web Frameworks Routing Routing in Rails Routing in Rails map.resources :blog_posts roughly short for: map.resources :blog_posts, :member => {:show => :get, :edit => :get, :update => :put}, : collection => {:index => :get, :new => :get, :create => :post} The second code portion is not identical to the first, but it s the same idea. The table is the actual output of the first code.
Routing in Rails Routing map.resources :blog_posts roughly short for: map.resources :blog_posts, :member => {:show => :get, :edit => :get, :update => :put}, : collection => {:index => :get, :new => :get, :create => :post} sets up: Helper Stem Method URL Handler blog posts GET /blog posts(.:format) :controller=>"blog posts", :action=>"index" POST /blog posts(.:format) :controller=>"blog posts", :action=>"create" new blog post GET /blog posts/new(.:format) :controller=>"blog posts", :action=>"new" edit blog post GET /blog posts/:id/edit(.:format) :controller=>"blog posts", :action=>"edit" blog post GET /blog posts/:id(.:format) :controller=>"blog posts", :action=>"show" PUT /blog posts/:id(.:format) :controller=>"blog posts", :action=>"update"
Yesod Routing
Routing in Yesod Routing / BlogIndexR GET /new blog post NewBlogPostR GET POST /blog posts/#blogpostid BlogPostR GET /blog posts/#blogpostid/edit EditBlogPostR GET POST
Routing in Yesod Routing / BlogIndexR GET /new blog post NewBlogPostR GET POST /blog posts/#blogpostid BlogPostR GET /blog posts/#blogpostid/edit EditBlogPostR GET POST generates: data Route =... Handler BlogIndexR getblogindexr NewBlogPostR getnewblogpostr, postnewblogpostr BlogPostR BlogPostId getblogpostr, postblogpostr EditBlogPostR BlogPostId geteditblogpostr
2012-05-01 Comparing Dynamic and Static Language Approaches to Web Frameworks Routing Routing in Yesod Routing in Yesod / BlogIndexR GET /new blog post NewBlogPostR GET POST /blog posts/#blogpostid BlogPostR GET /blog posts/#blogpostid/edit EditBlogPostR GET POST generates: data Route =... Handler BlogIndexR getblogindexr NewBlogPostR getnewblogpostr, postnewblogpostr BlogPostR BlogPostId getblogpostr, postblogpostr EditBlogPostR BlogPostId geteditblogpostr The Yesod routes don t quite have identical URLs to the Rails ones, but they are accomplishing the same idea. Also, the Route type is actually an associated data type of the class instance, but that doesn t really change things.
Routing Routing Both similar, using DSL to generate routing table and helpers Missing handler error: Rails run-time, Yesod compile-time
Web Frameworks Views
Views Views View: code that dynamically generates the actual HTML. Mix of text, HTML markup, and dynamic content, e.g. <p>this page has been viewed <b><% page_count() %></b> times</p>
Rails Views
Views in Rails: Showing a Blog Post Views Controller sets up variables ready for view: class BlogPostsController < ApplicationController def show @blog_post = BlogPost.find(params[:id]) end end
Views in Rails: Showing a Blog Post Views View has interspersed Ruby code: <p> <b>author:</b> <%= @blog_post.author.name %> </p> <p> <b>content:</b> <%= @blog_post.content %> </p> <%= link_to Edit, edit_blog_post_path(@blog_post) %> <%= link_to Index, root_path %>
2012-05-01 Comparing Dynamic and Static Language Approaches to Web Frameworks Views Views in Rails: Showing a Blog Post Views in Rails: Showing a Blog Post View has interspersed Ruby code: <p> <b>author:</b> <%= @blog_post.author.name %> </p> <p> <b>content:</b> <%= @blog_post.content %> </p> <%= link_to Edit, edit_blog_post_path(@blog_post) %> <%= link_to Index, root_path %> The Rails view is loaded in, substituted and compiled at run-time.
Yesod Views
Views in Yesod: Showing a Blog Post Views Handler sets up variables ready for view: getblogpostr :: BlogPostId -> Handler RepHtml getblogpostr blogpostid = do (blogpost, author) <- rundb $ do blogpost <- get404 blogpostid author <- get404 (blogpostauthor blogpost) return (blogpost, author) defaultlayout $(widgetfile "blog_post_view") The last line loads and compiles the view.
2012-05-01 Comparing Dynamic and Static Language Approaches to Web Frameworks Views Views in Yesod: Showing a Blog Post Views in Yesod: Showing a Blog Post Handler sets up variables ready for view: getblogpostr :: BlogPostId -> Handler RepHtml getblogpostr blogpostid = do (blogpost, author) <- rundb $ do blogpost <- get404 blogpostid author <- get404 (blogpostauthor blogpost) return (blogpost, author) defaultlayout $(widgetfile "blog_post_view") The last line loads and compiles the view. The get404 function is one that fails (and exits the handler, rendering a 404 page) if the database item is not found. The last line is a Template Haskell call which renders the view from the blog post view file (i.e. the file is explicitly named, rather than implicitly named by the controller action, as in Rails). The Template Haskell call reads in the file at compile-time, generates Haskell code based on the file, and then compiles that. So the template is completely compiled into code, and thus the splicing process is potentially optimised.
Views in Yesod: Showing a Blog Post Views View is Hamlet (indentation-based HTML), with interspersed Haskell code: <p> <b>author: #{authorname author} <p> <b>title: #{blogposttitle blogpost} <p> <b>content: #{blogpostcontent blogpost} <a href=@{editblogpostr blogpostid}>edit <a href=@{blogindexr}>index
2012-05-01 Comparing Dynamic and Static Language Approaches to Web Frameworks Views Views in Yesod: Showing a Blog Post Views in Yesod: Showing a Blog Post View is Hamlet (indentation-based HTML), with interspersed Haskell code: <p> <b>author: #{authorname author} <p> <b>title: #{blogposttitle blogpost} <p> <b>content: #{blogpostcontent blogpost} <a href=@{editblogpostr blogpostid}>edit <a href=@{blogindexr}>index Hamlet is like HTML, but has no closing tags, and uses indentation to indent tag-scoping. The #{blah} markup encloses Haskell code that returns text (which is then HTML escaped). The @{RootR} markup encloses a Haskell expression of Route type, and generates the URL for that item.
Views Views Hamlet is odd choice (and not necessary) Bad code in view error: Rails run-time, Yesod compile-time Haskell s field names are annoying (blogposttitle vs title)
2012-05-01 Comparing Dynamic and Static Language Approaches to Web Frameworks Views Views Views Hamlet is odd choice (and not necessary) Bad code in view error: Rails run-time, Yesod compile-time Haskell s field names are annoying (blogposttitle vs title) Haskell s field names (you must have unique field names across the program, not just unique per class like in OOP languages) are caused by the type inference in the language. It s a hot topic right now, with several proposals on how to fix it, but it does get irritating in applications like this.
Web Frameworks Forms
Forms Forms HTML forms are needed to enter data
Rails Forms
Forms in Rails Forms Controller sets up variables for view: class BlogPostsController < ApplicationController def edit @blog_post = BlogPost.find(params[:id]) end end
Forms in Rails: Editing a Blog Post Forms Form is a view with form-related code/idiom: <% form_for(@blog_post) do f %> <p> <%= f.label : title %><br /> <%= f.text_area : title %> </p> <p> <%= f.label :content %><br /> <%= f.text_area :content %> </p> <%= f.submit Update %> <% end %>
Forms in Rails: Editing a Blog Post Forms Submitted form comes back to controller: class BlogPostsController < ApplicationController def create @blog_post = BlogPost.find(params[:id]) if @blog_post.update_attributes(params[:blog_post]) redirect_to (@blog_post) else render :action => "edit " end end end
Yesod Forms
Forms in Yesod: Editing a Blog Post Forms Handler gets variables and creates form, which is passed to view: geteditblogpostr :: BlogPostId -> Handler RepHtml geteditblogpostr blogpostid = do blogpost <- rundb (get404 blogpostid) ( result, bpform) <- fst <$> runformpost (renderdivs (blogpostform blogpost)) case... of _ -> defaultlayout $(widgetfile "blog_post_edit")
Forms in Yesod: Editing a Blog Post Forms Form is generated from code: blogpostform :: BlogPost -> AForm App App BlogPost blogpostform blogpost = BlogPost (blogpostauthor blogpost) <$> areq textfield " Title " ( blogposttitle blogpost) < > areq textfield "Content" (blogpostcontent blogpost) Then spliced into view: <form method=post action=@{editblogpostr blogpostid}> ^{bpform} <input type=submit>
2012-05-01 Comparing Dynamic and Static Language Approaches to Web Frameworks Forms Forms in Yesod: Editing a Blog Post Forms in Yesod: Editing a Blog Post Form is generated from code: blogpostform :: BlogPost -> AForm App App BlogPost blogpostform blogpost = BlogPost (blogpostauthor blogpost) <$> areq textfield " Title " (blogposttitle blogpost) < > areq textfield "Content" (blogpostcontent blogpost) Then spliced into view: <form method=post action=@{editblogpostr blogpostid}> ^{bpform} <input type=submit> The splice syntax shown here, ˆ {blah}, splices in HTML content, which is not escaped.
Forms in Yesod: Editing a Blog Post Forms Submitted form comes back to handler: posteditblogpostr :: BlogPostId -> Handler RepHtml posteditblogpostr = geteditblogpostr
Forms in Yesod: Editing a Blog Post Forms Submitted form comes back to handler: geteditblogpostr :: BlogPostId -> Handler RepHtml geteditblogpostr blogpostid = do blogpost <- rundb (get404 blogpostid) ( result, bpform) <- fst <$> runformpost (renderdivs (blogpostform blogpost)) case result of FormSuccess blogpost -> do rundb (replace blogpostid blogpost ) redirect (BlogPostR blogpostid) _ -> defaultlayout $(widgetfile "blog_post_edit")
2012-05-01 Comparing Dynamic and Static Language Approaches to Web Frameworks Forms Forms in Yesod: Editing a Blog Post Forms in Yesod: Editing a Blog Post Submitted form comes back to handler: geteditblogpostr :: BlogPostId -> Handler RepHtml geteditblogpostr blogpostid = do blogpost <- rundb (get404 blogpostid) ( result, bpform) <- fst <$> runformpost (renderdivs (blogpostform blogpost)) case result of FormSuccess blogpost -> do rundb (replace blogpostid blogpost ) redirect (BlogPostR blogpostid) _ -> defaultlayout $(widgetfile "blog_post_edit") The runformpost function both creates the form, and attempts to parse the results of the form, which are stored in the environment (technically: in the state monad part of the Handler monad stack). Thus you can act on form success during the POST submit action, or just show the view during the original GET action. On the one hand, it makes sense for the GET and POST handler to be the same because they tend to share a lot of code (one builds the form from content, the other tears the form apart to get similar content back), but it is a little bit subtle that runformpost has these two different functionalities.
Forms Forms Single-handler is neat trick in Yesod, but overall Yesod generally takes more code to do forms Bad form field error: Rails run-time, Yesod compile-time
Final Notes Errors Yesod has compile-time errors where Rails has run-time errors How likely are mis-spelt names, etc? Flipping Rails plurals! Use of strong typing (in Yesod or Rails) confers other benefits, e.g. protection from XSS through data-tagging.
2012-05-01 Comparing Dynamic and Static Language Approaches to Web Frameworks Final Notes Errors Errors Yesod has compile-time errors where Rails has run-time errors How likely are mis-spelt names, etc? Flipping Rails plurals! Use of strong typing (in Yesod or Rails) confers other benefits, e.g. protection from XSS through data-tagging. Yesod has a different type for plain text and HTML data. The #{blah} splice takes plain text, and escapes it, whereas the ˆ {blah} splice takes HTML and does not escape it. Conversion between the types must be done explicitly (typically with pre-supplied functions). I gather Rails 3.0 has similar protection, but done dynamically.
Final Notes Development Want fast edit-test cycle in dev mode: Rails: reload source files before handling each request Yesod: watchdog recompiles after every change
Final Notes Overall Summary You can accomplish very similar results with static and dynamic approaches Metaprogramming and DSLs have lots of practical applications in web frameworks Lots of interesting uses of Haskell s type system in Yesod Rails: easy start, high maintenance 1 Yesod: tougher (even taking Haskell into account), lower maintenance (e.g. easier to refactor) 2 1 Speaking from experience! 2 Educated guess!