We are junior engineers, Bu Sung Kim and Jae Ho Lee at LINE, working on the LINE GAME Platform. We have a great interest in functional programming languages. It all started with learning that the LINE GAME Cloud is developed in Clojure, one of functional programming languages. On this post, we would like to take you through some of the characteristics of functional programming languages in association with the use cases of the LINE GAME Cloud.
LINE GAME Cloud & Functional Programming
LINE GAME Cloud is a game server platform, obviously cloud-based, to serve the LINE Games service worldwide, safe and sound. The cloud project was launched to globalize the LINE Games service and to automate distribution process. LINE GAME Cloud is currently in action serving the users all over the world, automatically issues servers, and supports L4/L7 routing, DNS and auto scaling. You can check the details of this project through the following links:
LINE GAME Cloud Structure
LINE GAME Cloud is structured as illustrated in the diagram below. There are three main modules, Orchestration Engine, Provisioning Agent, and Node Agent.
All of the following main modules are implemented in Clojure:
- Orchestration Engine: Handles provisioning for docker containers and provides settings information.
- Provisioning Agent: Operates on a single physical machine, together with the orchestration engine. Controls the docker containers based on the information generated by the orchestration engine.
- Node Agent: Manages service instances on each docker container.
Why use functional programming language for LINE GAME Cloud?
Two people. That was all we had when we were starting the LINE GAME Cloud. Lacking both resources and time, supporting a world-wide service meant that we had no option but to take a whole new different approach. Firstly, to speed up the process of developing and verifying a prototype, our program had to be written as simple as possible. At the same time, the program had to be capable of supporting concurrency for efficient handling of massive data across the world. Moreover, a stable runtime environment was something that we could not give up. On top of this, we wanted to reutilize the programs we had implemented as much as possible.
Functional programming languages seemed to satisfy our requirements. With it, we can write simple code, saving time. We can also write a problem-free program running on multiple threads because functional programming languages are pure-function-oriented, blocking side-effects. Clojure was the one we were going for.
Characteristics of functional programming languages
Here are a few of the characteristics of functional programming language we saw through LINE GAME Cloud.
The paradigm of functional programming languages is immutability, meaning that they leave no room for mutability as much as possible. Some say that they are pure-function-oriented. Pure functions guarantee the same output as long as the input value is the same, because pure functions do not have internal states. In other words, pure functions have no side effects. The trigonometric functions are pure functions, because trigonometric functions have no internal states and the output depends only on the function’s input.
Immutability brings in the following merits into the picture:
Immutability makes program verification easy. Only the input values affect program components, and there are no internal states that can change unexpectedly. Thanks to these characteristics, writing test code for the program is fairly easy.
Immutability allows various ways to deliver optimization. Memoization, which is about caching and reusing a function’s result, requires immutability to be guaranteed. Why? Because, there will be no point of caching a result of a function which returns different values all the time. Immutability allows runtime environments like JVM or CLR to rearrange the order of code on random basis. Since a function’s result depends only on its input and not on the order of functions called, a runtime environment is free to rearrange the order of code to execute to obtain the best performance.
LINE GAME Cloud uses memoization as shown below. The function takes three parameters, , , and and returns an instance of the class, . This function is a pure function. The function does not have internal states and the output of the function is affected only by its input values. This is why memoization (the last line of the code block) can be used. After being called once, if this function is invoked again with the exact same combination of arguments (, , ), the output that had been cached will be returned instead of executing the body of the function.
(defn make-auth-config- [username password server-address] (-> (AuthConfig/builder) (.username username) (.password password) (.serverAddress server-address) (.build)))
(def make-auth-config (memoize make-auth-config-))
Easy to write concurrent programs
Functional programming languages are useful in writing concurrent programs running on multiple processors. One of the reasons for experiencing difficulty in writing concurrent programs is having multiple threads sharing program states. Functional programming languages completely excludes the possibility of mutability, allowing programmers to focus on implementing the core logic and not worry about those pain-in-the neck issues on threads, such as locks or synchronization.
First-class, higher-order functions
Functions are first-class citizens in functional programming. As first-class citizens, functions can be assigned to variables, passed as arguments and returned as a function’s result. You can handle functions as if they are values, which means you can reuse functions like you reuse classes in object-oriented programs and also write code without boilerplate code. We believe that the reason for many languages supporting lambda expressions are to support pure functions without boilerplate code.
Also, we can use higher-order functions. A higher-order functions is a function created using the function passed in as an argument. Higher-order functions allow partial application and currying, helping programmers to write concise and simple code. Let’s see how higher-order functions are used in LINE GAME Cloud.
(defn listing-all-containers [docker] (let [lists (.listContainers docker (into-array [(com.spotify.docker.client.DockerClient$ListContainersParam/allContainers)]))] (map (fn [list] [:id (.id list) :names (.names list) :image (.image list) :imageId (.imageId list) :command (.command list) :created (.created list) :status (.status list) :ports (.ports list) :labels (.labels list) :sizeRw (.sizeRw list) :sizeRootFs (.sizeRootFs list)]) lists)))
The function in the code above is used to list all Docker containers. This function obtains a list of containers by calling the function of the docker instance, the function’s argument, and applies lambda expressions on each item in the list to create a new collection. Excluding the declarations, the function can be expressed as the following.
(map (fn ...) lists)
The function takes in two parameters. The first is a lambda expression to apply on each list item, and the second is the collection to apply the lambda expression. As we said earlier, functions can be used as arguments to other functions in functional programming, because functions are first-class citizens. The function is an example of a higher-order function; this function is a new function created by a combination of the function and a lambda expression.
Functional programming languages support lazy evaluation. Lazy evaluation delays calculating a value until the value is actually used. This allows saving memory since we don’t need to calculate and save the value before the value is actually used, which is good for program’s performance. Another merit of using lazy evaluation is being able to use an infinite sequence; we don’t have to store all the values of the sequence in memory, but calculate a value only when needed. Lazy evaluation is often used together with memoization; calculate a value when needed, cache the value and use the cached value when needed again. A good example of lazy evaluation is the Fibonacci sequence.
Let’s have a look with an example. The following function creates a Fibonacci sequence named , as an infinite sequence. Since no values are needed yet, the following code does not perform any operation.
(defn fib  (map first (iterate (fn [[a b]] [b (+ a b)]) [1 1])))
Once you actually pass an argument as shown below, only then the calculation begins.
(take 10 (fib)) ;Output => (1 1 2 3 5 8 13 21 34 55)
We briefly went through a few characteristics of functional programing languages, with some of the use cases on LINE GAME Cloud. (We would like to express our appreciation to Youngho Choi for sharing the reason why LINE GAME Cloud opted for functional programming language.) Programs written in functional programming languages have strength in terms of productivity, maintainability and concurrency. The use of functional programming languages is growing in the game industry and we are hearing many successful stories. LINE is also enthusiastic about making the best use of functional programming languages in developing game’s server-side. We hope to bring you more of the good news in near future.