Multi-paradigm Programming

profile image

Learn how to elegantly solve complex problems by strategically combining functional and object-oriented approaches through HTML template engine implementation.

This post has been translated by DeepL . Please let us know if there are any mistranslations!

Multiparadigm Programming

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.).

typescript
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.

typescript
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.

typescript
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 pushing 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.
typescript
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.

typescript
const escapeMap = {
  '&': '&amp;',
  '<': '&lt;',
  '>': '&gt;',
  '"': '&quot;',
  "'": '&#x27;',
  '`': '&#x60;',
};

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.

typescript
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>&lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;</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.

typescript
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>&lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;</li>
//   <li>Hello &amp; Welcome!</li>
//   &lt;li&gt;Choco Latte &amp; Cookie (8000)&lt;/li&gt;
//   &lt;li&gt;&lt;b&gt;3-step nesting&lt;/b&gt;&lt;/li&gt;
// </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!

typescript
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.

typescript
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>&lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;</li>
//   <li>Hello &amp; Welcome!</li>
//   <li>Choco Latte &amp; 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>.

typescript
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>
//     &lt;li&gt;Americano (4500)&lt;/li&gt;
//     &lt;li&gt;Cappuccino (5000)&lt;/li&gt;
//     &lt;li&gt;Latte &amp; cookie set (8000)&lt;/li&gt;
//   </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.

typescript
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.

typescript
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.

typescript
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 &amp; 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.

❤️ 0
🔥 0
😎 0
⭐️ 0
🆒 0