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.