
This post is a personal summary after reading Multiparadigm Programming (by Yoo In-dong).
Most modern programming languages have become multi-paradigm. They allow us to select and combine different paradigms such as functional, object-oriented, and imperative based on the situation. This approach goes beyond diversifying code styles and offers strategies for solving various practical problems with higher readability, maintainability, and scalability.
In this chapter, we'll explore how to strategically combine various language features to solve challenging problems with elegant and intuitive code.
Building an HTML Template Engine
Tagged Templates
Tagged Templates are tools that allow for more flexible use of template literals. They enable processing template strings and inserted values through user-defined functions.
This allows for flexible string handling. They can be used for various tasks such as string manipulation, internationalization, and security checks (preventing SQL injection, XSS, etc.).
function upper(strs: TemplateStringsArray, ...vals: string[]) {
console.log(strs); // ["a: ", ", b: ", "."]
console.log(vals); // ["a", "b"]
return strs[0]
+ vals[0].toUpperCase()
+ strs[1]
+ vals[1].toUpperCase()
+ strs[2];
}
const a = 'a';
const b = 'b';
const result = upper`a: ${a}, b: ${b}.`;
console.log(result); // a: A, b: B.
When passing a template literal to a function, the string parts of the template literal are passed as an array to the first parameter. Then, the values inside the ${}
expressions are passed as subsequent arguments.
Implementation with List Processing
Let's create an HTML template engine using Tagged Templates. First, we'll match the lengths of str
(fixed string array) and vals
(values inside expressions) from the template literal, then combine the two arrays using the zip
function to return tuples.
function html(strs: TemplateStringsArray, ...vals: string[]) {
vals.push('');
return pipe(
zip(strs, vals),
toArray
);
}
const a = 'A',
b = 'B',
c = 'C';
const result = html`<b>${a}</b><i>${b}</i><em>${c}</em>`;
console.log(result);
// [["<b>", "A"], ["</b><i>", "B"], ["</i><em>", "C"], ["</em>", ""]]
Next, let's add flat
to flatten the iterator and use reduce
to create a single string.
function html(strs: TemplateStringsArray, ...vals: string[]) {
vals.push('');
return pipe(
vals,
zip(strs),
flat,
reduce((a, b) => a + b),
);
}
const a = 'A',
b = 'B',
c = 'C';
const result = html`<b>${a}</b><i>${b}</i><em>${c}</em>`;
console.log(result);
// <b>A</b><i>B</i><em>C</em>
Replacing push with concat
Since the string part of the template strs
is always 1 larger than the expression value part vals
, we previously matched their lengths by push
ing an empty value to vals
. However, we can also use concat
instead.
- While
push
modifies the existing array,concat
returns a lazily evaluated iterator without side effects, allowing us to achieve the same result without side effects. - In terms of time complexity, there's essentially no difference as it doesn't create a whole new array or reassign all values.
const html = (strs: TemplateStringsArray, ...vals: string[]) =>
pipe(
concat(vals, ['']),
zip(strs),
flat,
reduce((a, b) => a + b)
);
The notable point in this change is not the reduction of side effects, but the ability to compose code using only expressions. Composing code with expressions alone increases code predictability by eliminating the possibility of value transformation or reference by subsequent statements.
Preventing XSS Attacks
XSS is an attack technique that inserts malicious scripts into web pages to harm other users viewing the page. Under the principle that no user input can be trusted, we need to convert characters that could be interpreted as HTML syntax into safe forms.
const escapeMap = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'`': '`',
};
const source = '(?:' + Object.keys(escapeMap).join('|') + ')';
const testRegexp = RegExp(source);
const replaceRegexp = RegExp(source, 'g');
function escapeHtml(val: unknown): string {
const string = `${val}`;
return testRegexp.test(string)
? string.replace(replaceRegexp, (match) => escapeMap[match])
: string;
}
We create a regular expression that replaces special characters with their corresponding HTML entities, and check the string to perform the conversion only when necessary.
Let's apply this function to each value in vals
using the map
function.
const html = (strs: TemplateStringsArray, ...vals: unknown[]) =>
pipe(
vals,
map(escapeHtml),
append(''),
zip(strs),
flat,
reduce((a, b) => a + b)
);
const a = '<script>alert("XSS")</script>';
const b = 'Hello & Welcome!';
console.log(html`
<ul>
<li>${a}</li>
<li>${b}</li>
</ul>
`);
// <ul>
// <li><script>alert("XSS")</script></li>
// <li>
// </ul>
Supporting Component-Based Development with Nested Data Processing
In modern web development, it's common to structure UI in reusable component units. To support this, template engines must be able to naturally handle nested structures.
Tagged Templates can be nested because they can return template literals within their functions. However, in the code below, the value returned by the menuHtml
function is recognized as a regular string by the html
function and is escaped by the escapeHtml
function.
type Menu = {
name: string;
price: number;
};
const menuHtml = ({ name, price }: Menu) => html`<li>${name} (${price})</li>`;
const menu: Menu = { name: 'Choco Latte & Cookie', price: 8000 };
const a = '<script>alert("XSS")</script>';
const b = 'Hello & Welcome!';
const result = html`
<ul>
<li>${a}</li>
<li>${b}</li>
${menuHtml(menu)}
${html`<li>${html`<b>3-step nesting</b>`}</li>`}
</ul>
`;
console.log(result);
// <ul>
// <li><script>alert("XSS")</script></li>
// <li>Hello & Welcome!</li>
// <li>Choco Latte & Cookie (8000)</li>
// <li><b>3-step nesting</b></li>
// </ul>
This is because the html
function escapes all values it receives by default. In the current structure, there's no way to convey the information that "this value is safe HTML and doesn't need to be escaped."
Solving Structural Problems with Object-Oriented, Logical Problems with Functional
- Strengths of Functional: Data transformation, maintaining immutability, composability
- Strengths of Object-Oriented: Complex state management, hierarchical structure, polymorphism
The problems we need to solve so far are:
- Hierarchical Structure Problem: When trying to express HTML syntax in a nested component form, it's difficult to get the intended output result with simple string concatenation.
- Selective Escaping Problem: Not all values need to be escaped, so certain values should be used as-is without escaping.
Such problems combining nested structures and selective transformation logic are not simple. A recursive approach is needed to traverse to the deepest part of the data to know the nesting depth, and if this is solved by adding if statements or while
loops impromptu, the code quickly becomes complex and difficult to maintain.
So what's the best approach? Let's use object-oriented polymorphism and encapsulation!
class Html {
constructor(
private strs: TemplateStringsArray,
private vals: unknown[]
) {}
private escape(val: unknown) {
return val instanceof Html
? val.toHtml()
: escapeHtml(val);
}
toHtml() {
return pipe(
this.vals,
map(val => this.escape(val)),
append(''),
zip(this.strs),
flat,
reduce((a, b) => a + b)
);
}
}
const html = (strs: TemplateStringsArray, ...vals: unknown[]) =>
new Html(strs, vals);
The class itself holds the data, and the logic determines how to escape and combine the data. Additionally, through the escape
method, if it's an Html
instance, it recursively calls toHtml()
, allowing it to handle multi-level nested HTML structures without issues.
const menuHtml = ({ name, price }: Menu) => html`<li>${name} (${price})</li>`;
const a = '<script>alert("XSS")</script>';
const b = 'Hello & Welcome!';
const menu: Menu = { name: 'Choco Latte & Cookie', price: 8000 };
const result = html`
<ul>
<li>${a}</li>
<li>${b}</li>
${menuHtml(menu)}
${html`<li>${html`<b>3-step nesting</b>`}</li>`}
</ul>
`;
console.log(result.toHtml());
// <ul>
// <li><script>alert("XSS")</script></li>
// <li>Hello & Welcome!</li>
// <li>Choco Latte & Cookie (8000)</li>
// <li><b>3-step nesting</b></li>
// </ul>
In solving this problem, we learned the methodology of "solve structural problems with object-oriented and logical problems with functional." This is a strategy that maximizes the strengths of each paradigm while complementing their weaknesses.
Creating HTML Strings from Arrays
So far, we've dealt with the HTML template engine focusing on nested data with key-value structures or single values. However, in real situations, array-type data also appears frequently. Most dynamic content such as user lists, product lists, and menu lists are array-based.
The following example attempts to render a menu array as <li>
inside a <ul>
.
const menuHtml = ({ name, price }: Menu) => html`<li>${name} (${price})</li>`;
const menuBoardHtml = (menus: Menu[]) => html`
<div>
<h1>Menu list</h1>
<ul>
${menus.map(menuHtml).reduce((acc, a) => acc + a.toHtml(), '')}
</ul>
</div>
`;
console.log(menuBoardHtml(menus).toHtml());
// <div>
// <h1>Menu list</h1>
// <ul>
// <li>Americano (4500)</li>
// <li>Cappuccino (5000)</li>
// <li>Latte & cookie set (8000)</li>
// </ul>
// </div>
The problems with this code are:
- toHtml() result is a simple string: Pure strings are passed to the upper
html
template function, which recognizes these values as regular strings and escapes them. - Lack of distinction between safe data and regular strings: There's no way to convey to the upper
html
function that this is "safely processed HTML."
Adding Objects Functionally
What if, instead of simply concatenating strings, we accumulate multiple Html instances into a single Html instance? This way, we can maintain the state information of "safe HTML" without losing it.
const menuBoardHtml2 = (menus: Menu[]) => html`
<div>
<h1>Menu list</h1>
<ul>
${menus.map(menuHtml).reduce((a, b) => html`${a}${b}`)}
</ul>
</div>
`;
By combining Html instances rather than strings, we can maintain the state of "already safe HTML" and solve nested data processing and escaping logic.
Moving Array Processing Inside the Class
For developer convenience, let's add a combine
method inside the Html
class. This way, simply inserting an array into the template will automatically generate HTML.
class Html {
constructor(
private strs: TemplateStringsArray,
private vals: unknown[]
) {}
// Added!
private combine(vals: unknown) {
return Array.isArray(vals)
? vals.reduce((a, b) => html`${a}${b}`, html``)
: vals;
}
private escape(val: unknown) {
return val instanceof Html
? val.toHtml()
: escapeHtml(val);
}
toHtml() {
return pipe(
this.vals,
map(val => this.escape(this.combine(val))), // Added!
append(''),
zip(this.strs),
flat,
reduce((a, b) => a + b)
);
}
}
With this change, you can get an HTML string simply by passing an array.
const menuBoardHtml = (menus: Menu[]) => html`
<div>
<h1>Menu list</h1>
<ul>
${menus.map(menuHtml)}
</ul>
</div>
`;
console.log(menuBoardHtml(menus).toHtml());
// <div>
// <h1>Menu list</h1>
// <ul>
// <li>Americano (4500)</li>
// <li>Cappuccino (5000)</li>
// <li>Latte & cookie set (8000)</li>
// </ul>
// </div>
Opportunities Presented by Multi-paradigm Languages
If we had tried to implement the other problems in this section with just one paradigm, it would have been much harder to solve. On the other hand, by actively utilizing the advantages of multi-paradigm languages, we can harmoniously solve different problems such as structural complexity (object-oriented) and transformation logic (functional).
This approach provides a foundation for employing flexible strategies and offers stable and scalable solutions that can respond to various problems, forming a solid basis for future programming tasks.
Conclusion
Through the process of creating an HTML template engine, we gained a perspective of separating structural and logical aspects into different paradigms. We were able to solve problems that would have been handled with complex code mixing loops and conditionals, using the strengths of each paradigm to create much more readable and maintainable code.
Additionally, we realized that it's not just about mixing various paradigms, but understanding the essential strengths of each paradigm and strategically selecting them according to the nature of the problem.
Going forward, I think we'll first consider "which paradigm is most suitable for this problem?" and then go through the strategic thinking of combining multiple paradigms as needed.