Использование библиотеки Ramda для написания Javascript, такого как Haskell.

Я искал способ написать Haskell в Интернете, есть Elm и Purescript, которые компилируются в Javascript, но взаимодействие с Javascript сложно, а интеграция с библиотеками трудна. Поскольку Javascript поддерживает функциональную парадигму, для него должны быть библиотеки, верно? Затем я нашел функциональную библиотеку для Javascript, которая позволяет нам писать функциональный код Javascript на уровне, подобном Haskell. Ramda имитирует функциональность Haskell Prelude и List, используя манипулирование списками и функциональный код.

В стиле функционального программирования мы не используем много циклов и условий. Вместо этого мы используем небольшие компонуемые функции, которые соединяем вместе для создания новых функций; рекурсия часто используется как замена зацикливанию чего-либо. Это похоже на то, как работает оболочка UNIX, где такие утилиты, как ls и grep, могут быть связаны друг с другом для формирования новых команд; кроме того, что мы делаем это с функциями.

Чтобы установить Ramda, нам сначала нужно использовать npm install, а затем мы можем импортировать его. Вы также можете попробовать Ramda, используя их веб-приложение. Запустите эту команду, чтобы сначала установить его.

npm install ramda --save

Затем мы можем импортировать функции из Ramda по отдельности.

import {map, ...} from "ramda"

Или все сразу.

import * as R from "ramda"

Прежде чем мы создадим полноценное примерное приложение, я расскажу о нескольких полезных функциях.

Списки

Ramda хорошо работает со списками; мы можем сделать большую часть функций списка, которые мы можем сделать из других функциональных языков. Мы можем получить начало списка с помощью head.

head([1,2,3]) // 1

Или весь остальной список с хвостиком.

tail([1,2,3,4]) // [2,3,4]

Мы также можем использовать take, чтобы взять определенное количество элементов из начала списка.

take(3, [1,2,3,4,5,6]); // [1,2,3]

Мы можем использовать такие функции, как any и all, чтобы определить, соответствуют ли какие-либо элементы и все элементы определенному предикату (булевой функции).

// true is any elements are even
any(x => x % 2 == 0, [1,2,3,4])

// true if all elements are even
all(x => x % 2 == 0, [1,2,3,4])

Есть некоторые функции из Haskell, которые мне очень нравятся, например, takeWhile; у нас тоже это есть; takeWhile берет из списка, пока предикат верен, и возвращает новый список.

// take while not 0
takeWhile(x => x != 0, [1,2,3,4,0,5,6,7]) // [1,2,3,4]

Частичный

Одной из наиболее важных концепций функционального программирования является частичное применение функций. Это когда к функции применяются только некоторые параметры, но не все. Это связано с понятием каррирования, когда функция принимает только один параметр за раз и возвращает новую функцию, к которой можно применить следующий параметр. Например, допустим, у меня есть функция addNums, которая принимает два числа.

const addNums = (a, b) => a + b

Я могу частично применить add, передав один параметр, чтобы сделать другую функцию.

const addTwo = addNums(2, b)

Это то, что позволяет нам делать partial; он создает новую функцию, к которой уже применены некоторые аргументы.

const addNums = (a, b) => a + b;
const addTwo = partial(addNums, [2]);

addTwo(3) // 5

Это становится полезным, когда мы хотим создать «фабрики функций», которые можно использовать по разным причинам, но при этом требуется некоторая абстракция от конечного результата. Как создание объектов с именем и адресом, но только для определенного адреса, например, если бы по одному адресу было несколько человек.

карри

Как я уже говорил, каррирование — это применение функции только с одним параметром за раз. Один интересный аспект Ramda заключается в том, что все функции по умолчанию каррированы. Например, для функции добавления мы можем указать только один параметр, а затем применить его без использования частичной функции.

const addTwo = add(2);
addTwo(3); // 5

add(2)(3) // does the same thing as the above

Это означает, что мы можем создавать новые функции на основе старых, которые применяют только определенные параметры, как мы делали для частичного.

Написать

Композиция функций — еще одно важное понятие. В математике мы можем составлять функции, которые применяют одну функцию за другой, создавая новую функцию. Например, если у нас есть одна функция f и другая g, мы можем скомпоновать с такой точкой f . g, например, мы можем написать f(g(x)) вместо (f . g)(x)

Мы можем сделать то же самое с Ramda; например, если мы хотим составить toLowerCase и разделить, мы можем сделать следующее.

const toLowerCase = s => s.toLowerCase();

const toLowerAndSplit = compose(split(" "), toLowerCase);

toLowerAndSplit("Hello World"); // ["hello", "world"]

Обратите внимание, что нам не нужно было применять каждую функцию по отдельности; мы могли бы просто составить, а затем применить.

карта

Карта — это функция, подобная карте Javascript, но ее можно использовать с любым «функтором», о котором мы поговорим чуть позже. В качестве примера использования карты давайте попробуем отобразить серию чисел с помощью карты. Это берет функцию и «функтор» и применяет функцию к функтору.

map(x => x * x, [1,2,3,4])

Сигнатура map — Functor f => (a → b) → f a → f b, что означает, что она принимает функцию и функтор, который упаковывает значение a, а затем возвращает новый функтор. Так что же такое функтор? Функтор — это тип, для которого определена функция map. Карта — это функция, которая отображает функтор и применяет к нему эту функцию. Таким образом, функтор — это категория «вещей, на которые можно сопоставить».

Примеры функторов включают списки, объекты и другие объекты, обеспечивающие эту функциональность. Ramda в базе предоставляет его для списков и объектов, но другие значения могут быть сопоставлены для включения строк и изображений, если существует достаточная реализация. Мы могли бы спросить, зачем использовать это вместо встроенной карты Javascript; однако карта Javascript применима только к спискам, а карта Ramda применима к любому функтору, включая объекты. Допустим, мы хотели написать имя и фамилию в объекте с большой буквы; тогда мы можем использовать такие имена с большой буквы.

map(s => s.toUpperCase(), {firstName: "Ron", lastName: "Jeremey"})

Уменьшать

Функция Reduce отличается тем, что сводит несколько значений к одному значению. Например, чтобы просуммировать кучу чисел, мы можем сделать следующее.

reduce((x, acc) => x + acc, 0, [1,2,3,4])

Первый аргумент — это функция, которую мы будем использовать для редукции, а второй аргумент — это начальное значение аккумулятора; последний — это объект, который мы отображаем. Аккумулятор — это результат нашей редукции, это может быть список, строка или объект. Другим примером сокращения могут быть строковые операции, такие как обращение строки, когда мы добавляем строку в обратном порядке.

reduce((c, s) => s + c, "", "Hello Ramda!");

Фильтр и многое другое

Комбинация этих функций обеспечивает работу алгоритма map-reduce, используемого при обработке данных. Эти функции можно комбинировать, например, чтобы подсчитать количество символов в строке, мы можем сначала использовать toLowerCase. Затем используйте фильтр, чтобы отсеять значения, которые не являются пробелами в строке, за исключением операторов if; это мало чем отличается от эквивалентного кода на Haskell.

const addToFreq =  (freq, c) => {
    if(c in freq) {
      freq[c] += 1;
    }
    else {
      freq[c] = 1;
    }
    return freq;
};

reduce(addToFreq, {},
  map(s => s.toLowerCase(),
      filter(c => c != " ", "This is a string of characters.")));

Написание приложения

Давайте воспользуемся этой функциональностью для реализации приложения, помогающего писать. Приложение возьмет часть текста и определит, какие слова наиболее распространены. Затем мы отобразим самые распространенные слова в таблице.

Для начала давайте создадим новый проект реакции, используя npx. Во-первых, убедитесь, что у вас установлены npx и node, затем запустите эти команды, которые создадут новое реагирующее приложение с помощью create-react-app.

npx create-react-app writing-helper
cd writing-helper
npm install ramda --save

npm run start

Теперь мы должны увидеть наше новое приложение. В src должен быть файл App.js; давайте заменим внутри App.js следующий код.

import logo from './logo.svg';
import './App.css';

import ReactDOM from 'react-dom';
import React, { useState } from "react";
import * as R from "ramda";

function App() {

    // set text state to "" initially
    const [text, setText] = useState("");

    return (
        <div className="App">
            <h2>Writing Helper</h2>
            <textarea
                id="textbox"
                className="textBox"
                rows={15} cols={80}
                onBlur={(e) => setText(e.target.value)}>
            </textarea>
            <br></br>
            <input type="button" value="Analyze" />
        </div>
    );
}

export default App;

Функция useState создает реагирующий стейт-хук, который мы будем использовать для хранения нашего текста. Затем мы добавляем HTML с текстовой областью для захвата текста. Обработчик события onBlur позволяет приложению сохранять текст всякий раз, когда текстовая область не в фокусе, например, при перемещении курсора.

Теперь нам нужен код для анализа текста и показа нам результата. Для этого создадим на кнопке обработчик onClick, который вызывает нужный нам функциональный код из Ramda. Мы можем использовать карту и уменьшить, как и раньше, чтобы подсчитать частоту слов. Затем мы можем использовать карту и сортировку, чтобы отсортировать частоты слов и найти наиболее распространенные слова. Код ниже покажет нам, что нам нужно сделать.

import logo from './logo.svg';
import './App.css';

import ReactDOM from 'react-dom';
import React, { useState } from "react";
import * as R from "ramda";

function App() {

    const [text, setText] = useState("");
    const [wordList, setWordList] = useState([]);


    const analyzeText = () => {

        const addToFreq = (freq, w) => {
            if (w in freq) {
                freq[w] += 1;
            }
            else {
                freq[w] = 1;
            }
            return freq;
        };
        // convert words to frequency mapping
        const freq = R.reduce(
            addToFreq,
            {},
            R.map(w => w.toLowerCase(), text.replace(/[.;:,!?-]/, "").split(/\s+/)));

        // function which sorts by freq
        const sortByFreq = (a, b) => {
            if (a[1] > b[1]) return -1;
            else if (a[1] < b[1]) return 1;
            else return 0;
        }

        // generate list of frequent words and sort by frequency and compose the two functions
        const listAndSort = R.compose(R.sort(sortByFreq), R.map(k => [k, freq[k]]));

        const freqList = listAndSort(Object.keys(freq));

        // take top 20 words
        const top20 = R.take(20, freqList);
        console.log(top20);
        setWordList(top20);
    }

    return (
        <div className="App">
            <h2>Writing Helper</h2>
            <textarea
                id="textbox"
                className="textBox"
                rows={15} cols={80}
                onBlur={(e) => setText(e.target.value)}>
            </textarea>
            <br></br>
            <input type="button" value="Analyze" onClick={analyzeText} />
            <table>
                {wordList.map((w, i) => {
                    return (
                        <tr key={i}>
                            <td>{w[0]}</td>
                            <td>{w[1]}</td>
                        </tr>
                    )
                })}
            </table>
        </div>
    );
}

export default App;

Мы также сопоставляем сгенерированный список слов, используя встроенную функцию сопоставления для списка, чтобы мы могли отображать элементы таблицы. Это должно занять линейное время для отображения каждого слова, поэтому оно должно работать быстро. Обратите внимание, насколько это похоже на код Haskell, OCaml или Lisp. Синтаксис в стиле Java в основном такой же. Javascript — это просто еще один функциональный язык с более подробным синтаксисом, чем другие языки.

Финал

Надеюсь, вы узнали о Ramda и получили удовольствие от ее использования. Есть и другие функциональные библиотеки, такие как Underscore, и они работают аналогично. Однако я обнаружил, что у Ramda есть API, очень похожий на Haskell. Кроме того, у него есть типы теории категорий, которые позволяют использовать функциональный стиль отображения объектов. Попробуйте использовать его для других приложений, которые вы хотите создать, и используйте функциональный стиль вместо набора циклов и условий. Почитайте о функторах, моноидах и т. д., чтобы понять силу функционального программирования.