Originally posted on medium.com
I love Ruby. I also love object-oriented programming. However, nowadays functional programming is getting more and more attention. And that’s not surprising because the functional paradigm has a lot of advantages.
Ruby, being a multi-paradigm language, allows us to write programs in a functional style. Let’s see if we can write a web application this way. Maybe we even end up inventing a web framework ;)
Ground rules
Let’s establish some ground rules.
- Lambdas. Lambdas everywhere
- Arrays. We can use arrays and some methods from
Enumerable
like#map
,#reduce
,#find
,#select
,#reject
- Hashes. We can access values via [key] and use merge for modifying them (no mutations!)
- Keeping external dependencies (like rack utility functions) to a minimum.
Rack
Rack provides a minimal, modular, and adaptable interface for developing web applications in Ruby.
Rack is used by Rails and Sinatra. You can learn more about it here: https://github.com/rack/rack
Rack expects an application to be an object with a #call
method accepting env
(hash containing all the information about a request) and returning a 3-element
tuple:
[status_code, headers_hash, body_array]
(body array is usually a list of strings)
This is great because it uses basic data structures and the #call
method
indicates that we can use a lambda/proc as an application.
Setup
Let’s set up an app first.
Gemfile
Generate a Gemfile with bundle init
and add Rack to it.
source "https://rubygems.org"
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
gem "rack"
config.ru
config.ru
is a script being run by rackup
program shipping with Rack and
starting a webserver.
require 'rack/reloader'
require_relative 'app'
use Rack::Reloader
run ->(env) { APP.call(env) }
Rack reloader will reload our application without restarting the server.
We will also assume that our app source code will define the APP
constant with
our app lambda. We also need to wrap it into another lambda or Rack won’t pick
up changes to the constant.
app.rb
# frozen_string_literal: true
APP = ->(env) { [200, {}, [env.inspect]] }
Now we can start the webserver with bundle exec rackup
.
It should be available on http://localhost:9292
Routing
Let’s add a route for /hello
showing “Hello world” message. Also, a 404 page.
Handlers
Let’s move our previous lambda from APP to a handler variable.
hello_handler = ->(env) { [200, {}, ["Hello world"]] }
env_inspect_handler = ->(env) { [200, {}, [env.inspect]] }
not_found_handler = ->(env) { [404, {}, ["404 Not found"]] }
Routes
Let’s define routes as a list of pairs condition + handler:
# Utility constant function
constant = ->(x) { ->(_) { x } }
exact_path_matcher = ->(path) { ->(env) { env['PATH_INFO'] == path } }
routes = [
[exact_path_matcher['/'], env_inspect_handler],
[exact_path_matcher['/hello'], hello_handler],
# else
[constant[true], not_found_handler]
]
We could also make a data type out of routes. For that, instead of explicitly
writing them as pairs we should create a function route(matcher, handler)
and
getter functions route_matcher(route)
and route_handler(route)
. This way
the code becomes more flexible since it won’t know a lot about the data
structure implementation. However, let’s keep it simple.
Router
A router is a dispatch function that decides which handler to use for a request.
comp = ->(f1, f2) { ->(x) { f1[f2[x]] } }
# Utility function to get the handler out of a route
second = ->((_a, b)) { b }
route_matcher = ->(env) { ->((cond, _handler)) { cond[env] } }
find_route = ->(env) { routes.find(&route_matcher[env]) }
find_handler = comp[second, find_route]
router = ->(env) { find_handler[env][env] }
APP = router
Middleware
Let’s make users provide their name to greet them properly.
For that, we need a params
hash.
Params middleware
A middleware is just a wrapper over a handler that modifies the env
or the
result returned from it.
Let’s create a middleware that adds params
hash to env
before handing it over to
a handler.
query_to_params = ->(q) { Rack::Utils.parse_nested_query(q) }
query_from_env = ->(env) { env['QUERY_STRING'] }
env_to_params = comp[query_to_params, query_from_env]
env_with_params = ->(env) { env.merge(params: env_to_params[env]) }
params_middleware = ->(handler) { comp[handler, env_with_params] }
APP = params_middleware[router]
The handler
Let’s use the new parameter in a handler.
hello_handler = ->(env) {
[200, {}, ["Hello #{env[:params]['name']}"]]
}
Now we should be able to see a greeting here: http://localhost:9292/hello?name=John+Doe
Content-Type header
Let’s also add text/html content-type header while we are at it.
content_type_middleware = ->(type) {
->(handler) {
->(env) {
status, headers, body = handler[env]
[status, headers.merge('Content-Type' => type), body]
}
}
}
html_content_type_middleware = content_type_middleware['text/html']
APP = html_content_type_middleware[params_middleware[router]]
Listing middleware
Right now we are adding a middleware by directly calling it on a handler. It would look nicer if we could just list them and apply later.
identity = ->(x) { x }
middleware_list = [
params_middleware,
html_content_type_middleware
]
app_middleware = middleware_list.reduce(identity, &comp)
APP = app_middleware[router]
The identity function is great. Composing it with another function has zero effect so it’s great as an init value for reducing.
Conclusion?
We have implemented a small web application. It doesn’t do much though. However, it’s interesting how far we could go by just using lambdas, arrays, and hashes.
Database interactions would introduce side-effects to our code. As an idea, they could be put into methods so it’s easier to distinguish them from pure lambdas.
We could also make a framework from it by moving the essential stuff to hashes and assigning them to constants. For example,
routes_to_router = ->(routes) {
route_matcher = ->(env) { ->((cond, _handler)) { cond[env] } }
find_route = ->(env) { routes.find(&route_matcher[env]) }
find_handler = comp[second, find_route]
->(env) { find_handler[env][env] }
}
ROUTING = {
routes_to_router: routes_to_router
}
You can find the complete source code here.