Форум программистов, компьютерный форум, киберфорум
golander
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск Заказать работу  

Интеграция JavaScript в Haskell

Запись от golander размещена 18.03.2025 в 08:11
Показов 1520 Комментарии 0

Нажмите на изображение для увеличения
Название: a65cc0be-92e4-41b4-b921-4b85748d3b76.jpg
Просмотров: 154
Размер:	188.1 Кб
ID:	10443
Ключевая особенность нового JS-бэкенда GHC — возможность создавать колбэки из JavaScript в Haskell-код. Это открывает дорогу разработке полноценных браузерных приложений, позволяя реагировать на действия пользователя прямо из Haskell. Фактически это означает, что теперь мы можем писать интерфейсы пользователя на Haskell и компилировать их непосредственно в JavaScript.

Haskell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
foreign import javascript unsafe 
""" 
((f) => { 
var node = document.createElement("button"); 
node.textContent = "Click me!"; 
node.onclick = () => { 
  node.textContent = `Clicked ${f()} times`;
}; 
document.body.appendChild(node);
})
"""
install_handler :: Callback (IO JSVal) -> IO ()
 
main :: IO ()
main = do
  ref <- newIORef 0
  let incRef = toJSInt <$> (modifyIORef' ref (+1) *> readIORef ref)
  syncCallback' incRef >>= install_handler
В этом простом примере мы создаём счетчик кликов, который увеличивается при каждом нажатии на кнопку. Обратите внимание на ключевой момент: колбэк, созданный в Haskell, передается в JavaScript-функцию. При этом сохраняется доступ к переменным Haskell-среды (замыкание над IORef), что обеспечивает корректное обновление счетчика.

До недавнего времени GHCJS — форк GHC, специализированный для компиляции Haskell в JavaScript — был основным инструментом для этого подхода. Однако его использование часто вызывало трудности: отдельный форк требует дополнительного сопровождения, застрял на версии GHC 8.10 и зачастую требует сложной настройки (обычно через Nix). Ситуация изменилась с появлением полноценного JavaScript-бэкенда в основной ветке GHC. Теперь мы можем использовать все преимущества современного Haskell непосредственно в браузере, причём всё работает "из коробки" без сложных настроек и дополнительных зависимостей.

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

Технические основы



Чтобы понимать, как работает интеграция JavaScript и Haskell, важно разобраться в механизмах компиляции и взаимодействия между этими языками. Современный GHC JavaScript бэкенд представляет собой технически сложное, но элегантное решение.

Механизм компиляции Haskell в JavaScript



GHC при использовании JavaScript-бэкенда компилирует Haskell-код не напрямую в JavaScript, а через промежуточное представление Cmm (C-минус-минус). Затем Cmm транслируется в код WebAssembly, который, в свою очередь, конвертируется в JavaScript с помощью Emscripten. Такой подход позволяет повторно использовать значительную часть инфраструктуры GHC без необходимости писать новый кодогенератор специально для JavaScript.

Результатом компиляции становится несколько JavaScript-файлов, включающих скомпилированный код Haskell, среду выполнения GHC и код взаимодействия с JavaScript API. Ключевые файлы:
all.js — основной бандл, включающий ваш код, зависимости и две системы выполнения.
all.externs.js — файл, декларирующий внешние переменные для минификатора.

Этот подход отличается от того, как работает, например, PureScript, который генерирует более "человекочитаемый" JavaScript-код, близкий к тому, что написал бы разработчик вручную. GHC производит низкоуровневый код, ориентированный на производительность.

Haskell
1
2
3
4
5
6
7
-- Простая функция в Haskell
factorial :: Int -> Int
factorial 0 = 1
factorial n = n * factorial (n - 1)
 
-- FFI для экспорта функции в JavaScript
foreign export javascript "factorial" factorial :: Int -> Int
Скомпилированный код будет сложнее, но позволит вызвать функцию factorial непосредственно из JavaScript.

Механизмы FFI для JavaScript



Foreign Function Interface (FFI) в GHC JavaScript бэкенде реализует двустороннее взаимодействие:
1. Экспорт Haskell-функций в JavaScript — позволяет вызывать Haskell-код из JavaScript.
2. Импорт JavaScript-функций в Haskell — позволяет вызывать JavaScript-код из Haskell.

Для импорта JavaScript-функций используются конструкции вида:

Haskell
1
2
foreign import javascript unsafe "Math.sqrt" 
    js_sqrt :: Double -> Double
Ключевое слово unsafe указывает, что импортируемая функция не блокирует поток выполнения Haskell и не выполняет никаких побочных действий с точки зрения памяти GHC. Существует также вариант safe, который обеспечивает дополнительные гарантии, но с небольшими накладными расходами. Для более сложных случаев можно импортировать анонимные функции JavaScript:

Haskell
1
2
3
4
5
6
7
foreign import javascript unsafe 
    "new Date().getTime()"
    js_currentTime :: IO Double
 
foreign import javascript unsafe
    "document.getElementById($1).textContent = $2"
    js_setElementText :: JSString -> JSString -> IO ()
Здесь $1, $2 и т.д. — это позиционные параметры, которые будут заменены аргументами функции при вызове. Наиболее важное нововведение в GHC 9.8 — поддержка колбэков из JavaScript в Haskell:

Haskell
1
2
3
4
5
6
7
8
9
10
11
12
13
-- Функция для создания колбэка
syncCallback' :: IO JSVal -> IO (Callback (IO JSVal))
 
-- Импорт JavaScript-функции, принимающей колбэк
foreign import javascript unsafe
    "document.getElementById($1).addEventListener('click', function() { $2(); })"
    js_addClickListener :: JSString -> Callback (IO ()) -> IO ()
 
-- Использование
setupButton :: JSString -> IO () -> IO ()
setupButton buttonId action = do
    callback <- syncCallback' (action *> pure jsNull)
    js_addClickListener buttonId callback
Этот механизм позволяет Haskell-коду реагировать на события DOM, что критично для создания интерактивных веб-приложений.

Типы данных и конвертация



Взаимодействие между Haskell и JavaScript требует преобразования типов данных. GHC JavaScript бэкенд предоставляет ряд примитивов для этого:

JSVal — непрозрачный тип для JavaScript-значений,
JSString — тип для JavaScript-строк,
toJSInt, fromJSInt — функции для конвертации чисел,
toJSString, fromJSString — функции для конвертации строк.

Haskell
1
2
3
4
5
6
7
8
9
10
-- Пример конвертации типов
updateCounter :: IORef Int -> IO JSVal
updateCounter ref = do
    count <- readIORef ref
    modifyIORef' ref (+1)
    return $ toJSInt count
 
foreign import javascript unsafe
    "console.log('Count: ' + $1)"
    js_logCount :: JSVal -> IO ()
Для сложных типов необходимо использовать JSON или другие сериализационные форматы:

Haskell
1
2
3
4
5
6
7
8
9
10
11
12
13
import Data.Aeson (encode, decode, FromJSON, ToJSON)
 
data User = User { name :: String, age :: Int } 
  deriving (Show, Generic, ToJSON, FromJSON)
 
-- Передача объекта в JavaScript
foreign import javascript unsafe
    "renderUser(JSON.parse($1))"
    js_renderUser :: JSString -> IO ()
 
displayUser :: User -> IO ()
displayUser user = 
    js_renderUser $ toJSString $ Text.unpack $ decodeUtf8 $ encode user

Оптимизация сгенерированного кода



Важный аспект работы с GHC JavaScript бэкендом — оптимизация сгенерированного кода. По умолчанию сгенерированный JavaScript может быть довольно объёмным. Например, код в примере достигал 1.8 МБ до оптимизации. Для минимизации размера сгенерированного кода можно использовать Google Closure Compiler:

Bash
1
2
3
4
5
6
npx google-closure-compiler --language_in UNSTABLE \
    --compilation_level ADVANCED_OPTIMIZATIONS \
    --warning_level QUIET --isolation_mode IIFE \
    --assume_function_wrapper --emit_use_strict \
    --js all.js --js all.externs.js \
    --js_output_file index.js
Этот подход значительно уменьшает размер — с 1.8 МБ до 396 КБ в примере из статьи. После сжатия с помощью brotli размер уменьшается ещё больше — до 76 КБ.
Для более современных проектов можно использовать swc — Rust-реализацию компилятора JavaScript, которая обеспечивает более глубокую интеграцию с современными инструментами сборки.

Конфигурация .swcrc может выглядеть примерно так:

JSON
1
2
3
4
5
6
7
8
9
10
{
  "minify": true,
  "jsc": {
    "minify": {
      "compress": true,
      "mangle": true,
      "sourceMap": false
    }
  }
}
При этом для webpack можно использовать swc-loader:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
module: {
  rules: [
    {
      test: /\.m?js$/,
      exclude: /(node_modules)/,
      use: {
        loader: "swc-loader"
      }
    }
  ]
}
Несмотря на то, что размер бандла при использовании swc может быть немного больше (844 КБ против 803 КБ с Google Closure Compiler), этот подход проще интегрировать с современными инструментами сборки и не требует дополнительных обходных путей для работы с зависимостями от npm.

Интеграция с инструментами сборки JavaScript



Одна из сложностей при разработке на Haskell для веба - это интеграция с инструментами сборки экосистемы JavaScript, которые нередко превышают по сложности сам JavaScript-код. Это особенно очевидно, когда мы начинаем использовать популярные инструменты вроде Parcel, Webpack или Rollup. Начнём с простого эксперимента: попробуем использовать Parcel, один из самых простых бандлеров, который известен своими разумными настройками по умолчанию и минимальной или нулевой конфигурацией.

Bash
1
npx parcel dev/index.html
Казалось бы, должно работать? К сожалению, мы сразу сталкиваемся с ошибкой:

Bash
1
2
3
Uncaught Error: process.binding is not supported
at process.binding (browser.js:177:11)
at Object.<anonymous> (index.js:144:368)
Что произошло? После некоторого расследования оказывается, что Parcel оборачивает модули в "заголовок", который предоставляет реализацию require. Это сбивает с толку код ghc-internal, который пытается определить, в какой среде он запущен. Заметив доступность require, он решает, что работает в Node.js, и вызывает process.binding, который не полифиллирован. У этой проблемы есть обходное решение - компилировать GHC специальным образом:

Bash
1
2
3
CONF_CC_OPTS_STAGE2="-sENVIRONMENT=web" emconfigure ./configure \
  --target=javascript-unknown-ghcjs \
  --with-js-cpp-flags="-DGHCJS_BROWSER -E -CC -Wno-unicode -nostdinc"
Это указывает компилятору исключить весь код проверки среды и использовать только веб-части среды выполнения Emscripten. Однако такой подход сложный в использовании и поддержке, особенно учитывая, что это опция времени компиляции GHC, а не времени линковки. Но есть лучшее решение - Webpack! В отличие от Parcel, Webpack позволяет нам более тонко контролировать, что именно полифиллируется:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// config/webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
  entry: './dev/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, '../dist'),
  },
  resolve: {
    fallback: {
      os: false,
      fs: false,
      child_process: false,
      path: false,
    }
  },
  plugins: 
  [ new HtmlWebpackPlugin({
      title: 'Blog'
  })]
};
В этой конфигурации мы явно указываем, что не нужно полифиллить модули Node.js, которые не нужны в браузерной среде. Такой подход работает прекрасно и не требует сложных изменений в самом GHC.

Управление памятью и сборка мусора



При работе с Haskell в браузере важно понимать, как работает управление памятью. JavaScript и Haskell имеют разные подходы к управлению памятью и сборке мусора. В Haskell для этого используется своя система, основанная на поколенческой сборке мусора, тогда как JavaScript имеет свою встроенную сборку мусора. Когда мы компилируем Haskell в JavaScript с помощью GHC JS бекэнда, изначальная система управления памятью Haskell должна быть реализована поверх JavaScript. Это создаёт несколько слоев сборки мусора:
1. Сборщик мусора Haskell, который работает в эмулированной среде.
2. Нативный сборщик мусора JavaScript в браузере.

Это может привести к неожиданному поведению и утечкам памяти, если не учитывать особенности этого взаимодействия. Например, если объект JavaScript ссылается на данные Haskell, а Haskell ссылается обратно на JavaScript, может возникнуть циклическая ссылка, которую трудно освободить. На практике нужно придерживаться нескольких правил:
1. Избегать сохранения больших объёмов данных на границе между языками.
2. Освобождать ресурсы JavaScript явно, когда они больше не нужны.
3. Внимательно отслеживать жизненный цикл объектов, созданных через FFI.

Haskell
1
2
3
4
5
6
7
8
9
10
11
-- Пример корректного освобождения ресурсов
data JSResource = JSResource (Foreign JSResource)
 
foreign import javascript unsafe "createResource"
  js_createResource :: IO JSResource
 
foreign import javascript unsafe "destroyResource"
  js_destroyResource :: JSResource -> IO ()
 
withJSResource :: (JSResource -> IO a) -> IO a
withJSResource action = bracket js_createResource js_destroyResource action

Асинхронное программирование и обработка событий



Веб-программирование глубоко асинхронно по своей природе, и это создаёт определённые сложности при работе с Haskell, который изначально проектировался с акцентом на чистых функциях и явной обработке эффектов. GHC JS бекэнд предлагает несколько подходов к асинхронному программированию:

1. Колбеки и обработчики событий



Наиболее простой подход — использование колбеков для обработки асинхронных событий. Мы уже видели пример с обработчиком клика:

Haskell
1
2
3
4
5
6
foreign import javascript interruptible
  "setTimeout(function() { $c(null); }, $1);"
  js_setTimeout :: Int -> IO ()
 
delay :: Int -> IO ()
delay ms = js_setTimeout ms
Ключевое слово interruptible указывает, что функция может прервать поток выполнения Haskell и возобновить его позже. Переменная $c — это функция продолжения, которая будет вызвана, когда асинхронная операция завершится.

2. Промисы и асинхронные операции



Для работы с промисами в JavaScript мы можем использовать аналогичный подход:

Haskell
1
2
3
4
5
6
7
8
9
foreign import javascript interruptible
  "fetch($1).then(response => response.json()).then($c);"
  js_fetchJSON :: JSString -> IO JSVal
 
fetchUserData :: String -> IO (Maybe User)
fetchUserData userId = do
  jsonVal <- js_fetchJSON (toJSString $ "/user/" ++ userId)
  let jsonStr = fromJSString $ js_stringify jsonVal
  return $ decode $ encodeUtf8 $ Text.pack jsonStr
Здесь мы ожидаем завершения промиса, прежде чем продолжить выполнение кода Haskell.

3. Интеграция с моделью событий DOM



Для обработки событий DOM используется механизм колбеков:

Haskell
1
2
3
4
5
6
7
8
foreign import javascript unsafe
  "document.getElementById($1).addEventListener($2, function(event) { $3(event); });"
  js_addEventListener :: JSString -> JSString -> Callback (JSVal -> IO ()) -> IO ()
 
setupForm :: IO ()
setupForm = do
  submitCallback <- syncCallback1' $ \_ -> handleSubmit
  js_addEventListener (toJSString "loginForm") (toJSString "submit") submitCallback

Стратегии взаимодействия с DOM



Прямое манипулирование DOM из Haskell возможно, но часто бывает сложным и неэффективным. Вместо этого можно использовать один из трёх основных подходов:

1. Виртуальный DOM



По аналогии с React и другими современными фреймворками, можно реализовать виртуальный DOM на Haskell. Затем изменения виртуального DOM будут эффективно применяться к реальному DOM. Примеры библиотек с таким подходом — haskell-halogen и miso.

Haskell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.HTML.Properties as HP
 
render :: State -> HH.HTML Query
render state = 
  HH.div [HP.class_ (HH.ClassName "container")]
    [ HH.h1_ [HH.text "Hello, World!"]
    , HH.button
        [ HP.class_ (HH.ClassName "btn")
        , HE.onClick (HE.input_ Increment)
        ]
        [ HH.text "Increment" ]
    , HH.div_ [HH.text $ "Count: " <> show state.count]
    ]

2. Компоненты с состоянием



Можно создавать компоненты, которые инкапсулируют своё состояние и логику. Этот подход похож на компонентную модель React или Vue.js:

Haskell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
data State = State { counter :: Int, ripple :: Maybe MDCRipple }
 
data Action = Initialize | Finalize | Click
 
component :: Component Query Input Output m
component = 
  H.component
    { initialState: \_ -> { counter: 0, ripple: Nothing }
    , render
    , eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
    }
  where
    handleAction :: Action -> H.HalogenM State Action slots Output m Unit
    handleAction = case _ of
      Initialize -> do
        -- Initialize component
      Click -> do
        H.modify_ \st -> st { counter = st.counter + 1 }
      Finalize -> do
        -- Cleanup resources

3. Реактивное программирование



Третий подход — использование реактивного программирования с библиотеками вроде reflex или reactive-banana. Это позволяет декларативно описывать, как состояние приложения меняется в ответ на события:

Haskell
1
2
3
4
5
6
7
8
import Reflex
import Reflex.Dom
 
app :: MonadWidget t m => m ()
app = el "div" $ do
  btn <- button "Click me"
  count <- count (domEvent Click btn)
  display count

Простой обфускатор JavaScript кода на Haskell
Здраствуйте. Нуждаюсь в помощи в написании простого обфускатора для JS кода. Обфускатор должен извлекать пробельные символы, комментарии. Имена...

Место ФП и Haskell в компьютерной индустрии (Для чего он нужен, этот Haskell?)
&quot;У нас&quot; ? А где преподавание этой экзотики на высоте? Добавлено через 2 минуты А где такие &quot;пришедшие&quot; используют...

Функции в haskell, адаптация из Ruby -> Haskell
Добрый день, помогите адаптировать функциональный код с Ruby на Haskell. Напишите функцию, строящую по заданному списку строк новый список, в...

Help with Haskell
1) Зачем параметры у Note? 2) getByLetter :: -&gt; Char -&gt; String -&gt; Какие параметры у этой функции? Особенно интересует последний входной...


Пошаговая реализация взаимодействия



Теперь, когда мы разобрались с техническими аспектами, давайте перейдем к практической части — пошаговой реализации взаимодействия между Haskell и JavaScript. Мы рассмотрим настройку среды разработки и создадим простой, но показательный пример интеграции.

Настройка проекта



Первым шагом в работе с JavaScript-бэкендом GHC является настройка проекта. Вам понадобится GHC версии 9.8 или выше с поддержкой JavaScript-бэкенда. Установить его можно через ghcup:

Bash
1
ghcup install ghc 9.12.1 --flavor javascript-unknown-ghcjs
После установки создадим базовую структуру проекта:

Bash
1
2
3
mkdir haskell-js-integration
cd haskell-js-integration
cabal init --minimal --non-interactive
Далее отредактируем файл .cabal, добавив необходимые параметры:

Bash
1
2
3
4
5
6
7
8
executable haskell-js-app
  main-is:             Main.hs
  build-depends:       base ^>=4.17.0.0
  hs-source-dirs:      src
  default-language:    Haskell2010
  js-sources:          js/app.js
  if impl(ghc >= 9.8)
    ghc-options:       -no-hs-main
Обратите внимание на параметр js-sources, который указывает на JavaScript-файлы, которые будут включены в сборку. Флаг -no-hs-main указывает компилятору, что точка входа будет на стороне JavaScript.

Создадим базовую структуру файлов:

Bash
1
2
mkdir src js
touch src/Main.hs js/app.js

Создание простой кнопки с счетчиком



Начнем с простого примера — кнопки, которая считает количество кликов. Сначала напишем JavaScript-часть:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
// js/app.js
window.HaskellApp = {
  createButton: function(initialText, onClick) {
    const button = document.createElement('button');
    button.textContent = initialText;
    button.addEventListener('click', function() {
      const newText = onClick();
      button.textContent = newText;
    });
    document.body.appendChild(button);
    return button;
  }
};
Теперь напишем Haskell-код для взаимодействия с этой функцией:

Haskell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
-- src/Main.hs
module Main where
 
import Data.IORef
import GHC.JS.Foreign
import GHC.JS.Prim
 
foreign import javascript unsafe
  "window.HaskellApp.createButton($1, $2)"
  js_createButton :: JSString -> Callback (IO JSString) -> IO JSVal
 
main :: IO ()
main = do
  counterRef <- newIORef 0
  
  -- Функция, которая будет вызываться при клике
  let onClick = do
        counter <- readIORef counterRef
        let newCounter = counter + 1
        writeIORef counterRef newCounter
        return $ toJSString $ "Кликов: " ++ show newCounter
  
  -- Создаем колбэк для JavaScript
  callback <- syncCallback' onClick
  
  -- Создаем кнопку с начальным текстом
  js_createButton (toJSString "Нажми меня") callback
  
  -- Блокируем выполнение main, чтобы программа не завершилась сразу
  js_blockForever
  
  where
    foreign import javascript unsafe
      "(function() { return new Promise(function() {}); })()"
      js_blockForever :: IO ()
Мы создали простое приложение, где JavaScript отвечает за создание и отображение кнопки, а Haskell управляет логикой и состоянием. Когда пользователь кликает кнопку, вызывается колбэк, написанный на Haskell, который увеличивает счетчик и возвращает новый текст для кнопки. Соберем и запустим наше приложение:

Bash
1
cabal build
После сборки вы получите JavaScript-файлы в каталоге dist-newstyle/build/js-unknown-ghcjs/ghc-9.xx/haskell-js-app-0.1.0.0/x/haskell-js-app/build/haskell-js-app/haskell-js-app.jsexe/. Для запуска просто создайте HTML-файл, который загружает all.js:

HTML5
1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Haskell JS Integration</title>
</head>
<body>
  <script src="all.js"></script>
</body>
</html>

Интеграция с Material Design



Теперь перейдем к более сложному примеру — интеграции с библиотекой Material Design для создания стилизованной кнопки. Для этого нам понадобится установить несколько npm-пакетов:

Bash
1
2
npm init -y
npm install @material/button @material/ripple
Создадим JavaScript-файл с функциями для работы с Material Design:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
// js/material.js
import {MDCRipple} from '@material/ripple';
 
window.MaterialComponents = {
  initRipple: function(element) {
    return new MDCRipple(element);
  },
  destroyRipple: function(ripple) {
    ripple.destroy();
  }
};
Обновим файл .cabal, чтобы включить новый JavaScript-файл:

Haskell
1
2
3
4
5
6
7
8
9
executable haskell-js-app
  main-is:             Main.hs
  other-modules:       MaterialButton
  build-depends:       base ^>=4.17.0.0
  hs-source-dirs:      src
  default-language:    Haskell2010
  js-sources:          js/app.js js/material.js
  if impl(ghc >= 9.8)
    ghc-options:       -no-hs-main
Теперь создадим модуль для работы с Material-кнопкой:

Haskell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
-- src/MaterialButton.hs
module MaterialButton where
 
import GHC.JS.Foreign
import GHC.JS.Prim
import Data.IORef
 
newtype MDCRipple = MDCRipple (Foreign MDCRipple)
 
foreign import javascript unsafe
  "window.MaterialComponents.initRipple($1)"
  js_initRipple :: JSVal -> IO MDCRipple
 
foreign import javascript unsafe
  "window.MaterialComponents.destroyRipple($1)"
  js_destroyRipple :: MDCRipple -> IO ()
 
foreign import javascript unsafe
  "document.createElement('button')"
  js_createButtonElement :: IO JSVal
 
foreign import javascript unsafe
  "$1.classList.add($2)"
  js_addClassName :: JSVal -> JSString -> IO ()
 
foreign import javascript unsafe
  "$1.textContent = $2"
  js_setTextContent :: JSVal -> JSString -> IO ()
 
foreign import javascript unsafe
  "$1.appendChild($2)"
  js_appendChild :: JSVal -> JSVal -> IO ()
 
foreign import javascript unsafe
  "document.body.appendChild($1)"
  js_appendToBody :: JSVal -> IO ()
 
foreign import javascript unsafe
  "$1.addEventListener('click', $2)"
  js_addEventListener :: JSVal -> Callback (IO ()) -> IO ()
 
createMaterialButton :: String -> (Int -> String) -> IO () -> IO JSVal
createMaterialButton initialText formatText onClick = do
  -- Создаем счетчик
  counterRef <- newIORef 0
  
  -- Создаем элементы DOM
  button <- js_createButtonElement
  js_addClassName button (toJSString "mdc-button")
  js_addClassName button (toJSString "mdc-button--raised")
  
  -- Создаем span для ripple эффекта
  rippleSpan <- js_createButtonElement
  js_addClassName rippleSpan (toJSString "mdc-button__ripple")
  
  -- Создаем span для текста
  textSpan <- js_createButtonElement
  js_addClassName textSpan (toJSString "mdc-button__label")
  js_setTextContent textSpan (toJSString initialText)
  
  -- Собираем элементы вместе
  js_appendChild button rippleSpan
  js_appendChild button textSpan
  js_appendToBody button
  
  -- Инициализируем ripple эффект
  ripple <- js_initRipple button
  
  -- Создаем обработчик клика
  let handleClick = do
        count <- readIORef counterRef
        let newCount = count + 1
        writeIORef counterRef newCount
        js_setTextContent textSpan (toJSString $ formatText newCount)
        onClick
  
  -- Подключаем обработчик события
  callback <- syncCallback' handleClick
  js_addEventListener button callback
  
  return button
Теперь обновим основной файл:

Haskell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
-- src/Main.hs
module Main where
 
import GHC.JS.Foreign
import MaterialButton
 
main :: IO ()
main = do
  -- Создаем Material-кнопку
  _ <- createMaterialButton "Нажми меня" formatClickCount (pure ())
  
  -- Блокируем выполнение main
  js_blockForever
  
  where
    formatClickCount 0 = "Нажми меня!"
    formatClickCount 1 = "Нажата 1 раз"
    formatClickCount n = "Нажата " ++ show n ++ " раз"
    
    foreign import javascript unsafe
      "(function() { return new Promise(function() {}); })()"
      js_blockForever :: IO ()

Сборка с использованием webpack



Чтобы корректно обрабатывать импорты из npm-пакетов и CSS, нам понадобится webpack. Создадим файл конфигурации:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
 
module.exports = {
  entry: [
    './dist-newstyle/build/js-unknown-ghcjs/ghc-9.xx/haskell-js-app-0.1.0.0/x/haskell-js-app/build/haskell-js-app/haskell-js-app.jsexe/all.js', 
    './styles/main.scss'
  ],
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
  resolve: {
    fallback: {
      os: false,
      fs: false,
      child_process: false,
      path: false,
    }
  },
  module: {
    rules: [
      {
        test: /\.s[ac]ss$/i,
        use: ['style-loader', 'css-loader', 'sass-loader'],
      },
      {
        test: /\.m?js$/,
        exclude: /(node_modules)/,
        use: {
          loader: 'swc-loader'
        }
      }
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'Haskell JS Integration'
    })
  ]
};
Создадим SCSS-файл для стилей:

CSS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// styles/main.scss
@use "@material/button/styles";
 
body {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
  margin: 0;
  font-family: 'Roboto', sans-serif;
  background-color: #f5f5f5;
}
 
.mdc-button {
  margin: 10px;
}
Установим необходимые npm-пакеты для сборки:

Bash
1
npm install --save-dev webpack webpack-cli html-webpack-plugin style-loader css-loader sass-loader sass @swc/core swc-loader
Теперь мы можем собрать наше приложение:

Bash
1
2
cabal build  # Компилируем Haskell-код
npx webpack  # Собираем окончательный бандл

Минимизация кода и его оптимизация



Оптимизация размера и производительности сгенерированного кода - одна из ключевых задач при разработке веб-приложений на Haskell. Размер бандла напрямую влияет на время загрузки страницы и общее впечатление пользователя.

Существует несколько подходов к оптимизации JavaScript-кода, получаемого из Haskell:

Bash
1
2
3
4
5
6
7
8
# Минификация с помощью google-closure-compiler
npx google-closure-compiler --language_in UNSTABLE \
  --compilation_level ADVANCED_OPTIMIZATIONS \
  --warning_level QUIET --isolation_mode IIFE \
  --assume_function_wrapper --emit_use_strict \
  --js dist-newstyle/build/.../all.js \
  --js dist-newstyle/build/.../all.externs.js \
  --js_output_file dist/index.js
В моих экспериментах минификация уменьшила размер сгенерированного JavaScript с 1.8МБ до 396КБ. Дополнительное сжатие brotli уменьшило размер до 76КБ - уже гораздо более приемлемое значение для передачи по сети. Однако есть одна проблема при использовании google-closure-compiler - он не очень хорошо работает с современными практиками импорта из npm-пакетов. Например, при попытке импортировать модуль из npm-пакета в нашем JavaScript-коде:

JavaScript
1
import {MDCRipple} from '@material/ripple';
Компилятор выдаст ошибку:

Haskell
1
ERROR - [JSC_INVALID_MODULE_PATH] Invalid module path "@material/ripple" for resolution mode "BROWSER"
Есть два пути решения этой проблемы:
1. Использование swc вместо google-closure-compiler.
2. Создание обходного пути через window и externs-файлы.

Первый вариант обычно предпочтительнее, так как он лучше интегрируется с современным инструментарием JS.

Работа с зависимостями от npm



При интеграции Haskell-кода с JavaScript-библиотеками мы часто сталкиваемся с необходимостью использовать пакеты из npm. В нашем примере с Material Design мы использовали @material/button и @material/ripple.
Чтобы упростить работу с npm-зависимостями, можно создать отдельный файл с экспортами, который будет служить мостом между Haskell и JavaScript:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// js/bridge.js
import {MDCRipple} from '@material/ripple';
import {MDCMenu} from '@material/menu';
import {MDCDialog} from '@material/dialog';
 
// Экспортируем все компоненты через глобальный объект
window.MaterialComponents = {
  ripple: { 
    init: element => new MDCRipple(element),
    destroy: ripple => ripple.destroy()
  },
  menu: {
    init: element => new MDCMenu(element),
    open: menu => menu.open = true,
    close: menu => menu.open = false
  },
  dialog: {
    init: element => new MDCDialog(element),
    open: dialog => dialog.open(),
    close: dialog => dialog.close()
  }
};
Затем в Haskell мы можем обращаться к этим функциям:

Haskell
1
2
3
4
5
6
7
foreign import javascript unsafe
  "window.MaterialComponents.ripple.init($1)"
  js_initRipple :: JSVal -> IO MDCRipple
 
foreign import javascript unsafe
  "window.MaterialComponents.ripple.destroy($1)"
  js_destroyRipple :: MDCRipple -> IO ()
Такой подход делает интерфейс между Haskell и JavaScript более чистым и предсказуемым.

Использование паттерна "Компонент"



Чтобы сделать наш код более модульным и поддерживаемым, можно использовать паттерн "Компонент" - объект, который инкапсулирует внутреннее состояние и предоставляет методы для взаимодействия. Пример реализации компонента кнопки:

Haskell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
data MaterialButton = MaterialButton 
  { buttonElement :: JSVal
  , ripple :: MDCRipple
  , counter :: IORef Int
  , callbacks :: [Callback (IO ())]  -- Храним колбеки, чтобы они не были собраны сборщиком мусора
  }
 
createButton :: String -> IO MaterialButton
createButton initialText = do
  -- Создаем элементы DOM и настраиваем их
  button <- js_createButtonElement
  js_addClassName button (toJSString "mdc-button")
  
  rippleElement <- js_createElement "span"
  js_addClassName rippleElement (toJSString "mdc-button__ripple")
  
  labelElement <- js_createElement "span"
  js_addClassName labelElement (toJSString "mdc-button__label")
  js_setTextContent labelElement (toJSString initialText)
  
  -- Собираем структуру DOM
  js_appendChild button rippleElement
  js_appendChild button labelElement
  
  -- Инициализируем состояние
  ripple <- js_initRipple button
  counter <- newIORef 0
  
  -- Возвращаем компонент
  return $ MaterialButton button ripple counter []
 
-- Метод добавления обработчика клика
addClickHandler :: MaterialButton -> (Int -> IO ()) -> IO MaterialButton
addClickHandler btn@MaterialButton{..} handler = do
  callback <- syncCallback' $ do
    count <- readIORef counter
    let newCount = count + 1
    writeIORef counter newCount
    handler newCount
  
  js_addEventListener buttonElement (toJSString "click") callback
  
  return btn { callbacks = callback : callbacks }
 
-- Метод для добавления кнопки в DOM
appendToBody :: MaterialButton -> IO ()
appendToBody MaterialButton{..} = js_appendToBody buttonElement
 
-- Метод для уничтожения компонента и освобождения ресурсов
destroy :: MaterialButton -> IO ()
destroy MaterialButton{..} = do
  js_destroyRipple ripple
  -- Освобождаем колбеки
  mapM_ js_releaseCallback callbacks
  -- Удаляем элемент из DOM
  js_removeFromDOM buttonElement
 
foreign import javascript unsafe
"($1).remove()"
js_removeFromDOM :: JSVal -> IO ()
 
foreign import javascript unsafe
"$1.release()"
js_releaseCallback :: Callback a -> IO ()
Использование этого компонента будет выглядеть так:

Haskell
1
2
3
4
5
6
7
8
9
10
main :: IO ()
main = do
  btn <- createButton "Нажми меня"
    >>= addClickHandler (\count -> 
          js_setTextContent (buttonText btn) (toJSString $ "Нажали " ++ show count ++ " раз"))
  
  appendToBody btn
  
  -- При необходимости можно уничтожить компонент
  -- destroy btn

Жизненный цикл компонентов



При разработке веб-приложений на Haskell важно учитывать жизненный цикл компонентов, особенно при работе с внешними JavaScript-библиотеками. Типичный жизненный цикл компонента включает следующие фазы:
1. Инициализация - создание DOM-элементов и начальная настройка.
2. Монтирование - добавление элементов в DOM.
3. Обновление - реакция на изменения состояния или входных данных.
4. Размонтирование - удаление из DOM и освобождение ресурсов.

Важно корректно обрабатывать каждую из этих фаз, особенно размонтирование, чтобы избежать утечек памяти. Многие JavaScript-библиотеки, включая Material Design Components, требуют явного вызова методов для освобождения ресурсов.
В нашем примере с Material Button мы реализовали метод destroy, который:
1. Останавливает ripple-эффект.
2. Освобождает колбеки, чтобы предотвратить утечки памяти.
3. Удаляет элемент из DOM.

Для более сложных компонентов может потребоваться более тщательное управление жизненным циклом, особенно если они имеют собственное внутреннее состояние или подписки на события.

Интеграция с библиотекой Halogen



Halogen — популярная библиотека для создания пользовательских интерфейсов в функциональном стиле. Изначально она была разработана для PureScript, но теперь портирована и для Haskell. Для использования Halogen в нашем проекте нужно добавить зависимость в файл .cabal:

Haskell
1
2
3
build-depends: 
  base ^>=4.17.0.0,
  halogen
Затем мы можем использовать Halogen для создания более сложных компонентов:

Haskell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import Halogen
import Halogen.HTML as HH
import Halogen.HTML.Properties as HP
import Halogen.HTML.Events as HE
import Halogen.HTML.Properties.ARIA as HPA
 
data Action = Initialize | Finalize | Click
data State = State { ripple :: Maybe MDCRipple, counter :: Int }
 
button :: Component q i o IO
button = mkComponent $ ComponentSpec
  { initialState = const $ State Nothing 0
  , render
  , eval = mkEval $ defaultEval 
      { handleAction = handleAction
      , initialize = Just Initialize
      , finalize = Just Finalize 
      }
  }
  where
    ref = RefLabel "mdc-button"
    
    render State{counter} = 
      HH.div [HP.class_ (HH.ClassName "mdc-touch-target-wrapper")]
        [ HH.button
          [ HP.classes [ HH.ClassName "mdc-button"
                      , HH.ClassName "mdc-button--outlined"
                      , HH.ClassName "mdc-button--icon-leading" 
                      ]
          , HE.onClick (const Click)
          , HP.ref ref
          ]
          [ HH.span [HP.class_ (HH.ClassName "mdc-button__ripple")] []
          , HH.span [HP.class_ (HH.ClassName "mdc-button__touch")] []
          , HH.i [HP.classes [HH.ClassName "material-icons", HH.ClassName "mdc-button__icon"], HPA.hidden "true"] 
              [HH.text "add"]
          , HH.span [HP.class_ (HH.ClassName "mdc-button__label")] 
              [HH.text $ if counter == 0 then "Click me!" else "Clicked " ++ show counter ++ " times"]
          ]
        ]

Примеры из практики



После освоения базовых принципов интеграции Haskell и JavaScript, давайте рассмотрим несколько более практических примеров. Эти примеры помогут увидеть, как применять полученные знания в реальных проектах.

Интерактивная форма с валидацией



Первый пример — создание формы с валидацией данных на стороне клиента. Здесь мы воспользуемся преимуществом сильной типизации Haskell для построения надежной системы валидации.

Haskell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
-- Определяем типы данных для формы
data FormState = FormState
  { username :: String
  , email :: String
  , password :: String
  , usernameError :: Maybe String
  , emailError :: Maybe String
  , passwordError :: Maybe String
  }
 
-- Начальное состояние
initialState :: FormState
initialState = FormState
  { username = ""
  , email = ""
  , password = ""
  , usernameError = Nothing
  , emailError = Nothing
  , passwordError = Nothing
  }
 
-- Действия формы
data FormAction
  = SetUsername String
  | SetEmail String
  | SetPassword String
  | ValidateUsername
  | ValidateEmail
  | ValidatePassword
  | SubmitForm
 
-- Валидаторы
validateUsername :: String -> Maybe String
validateUsername name
  | length name < 3 = Just "Имя пользователя должно содержать минимум 3 символа"
  | otherwise = Nothing
 
validateEmail :: String -> Maybe String
validateEmail email
  | not (contains '@' email) = Just "Некорректный email адрес"
  | otherwise = Nothing
  where
    contains c = elem c
 
validatePassword :: String -> Maybe String
validatePassword pwd
  | length pwd < 8 = Just "Пароль должен содержать минимум 8 символов"
  | not (any isUpper pwd) = Just "Пароль должен содержать хотя бы одну заглавную букву"
  | not (any isDigit pwd) = Just "Пароль должен содержать хотя бы одну цифру"
  | otherwise = Nothing
 
-- Компонент формы
formComponent :: Component FormQuery FormInput FormOutput IO
formComponent = mkComponent
  { initialState: const initialState
  , render
  , eval: mkEval $ defaultEval { handleAction = handleAction }
  }
  where
    render :: FormState -> H.HTML FormAction
    render state = 
      HH.form [ HE.onSubmit (\_ -> SubmitForm) ]
        [ HH.div [ HP.class_ (HH.ClassName "form-group") ]
            [ HH.label [ HP.for "username" ] [ HH.text "Имя пользователя" ]
            , HH.input
                [ HP.type_ HP.InputText
                , HP.id "username"
                , HP.value state.username
                , HE.onValueInput SetUsername
                , HE.onBlur (const ValidateUsername)
                ]
            , renderError state.usernameError
            ]
        , HH.div [ HP.class_ (HH.ClassName "form-group") ]
            [ HH.label [ HP.for "email" ] [ HH.text "Email" ]
            , HH.input
                [ HP.type_ HP.InputEmail
                , HP.id "email"
                , HP.value state.email
                , HE.onValueInput SetEmail
                , HE.onBlur (const ValidateEmail)
                ]
            , renderError state.emailError
            ]
        , HH.div [ HP.class_ (HH.ClassName "form-group") ]
            [ HH.label [ HP.for "password" ] [ HH.text "Пароль" ]
            , HH.input
                [ HP.type_ HP.InputPassword
                , HP.id "password"
                , HP.value state.password
                , HE.onValueInput SetPassword
                , HE.onBlur (const ValidatePassword)
                ]
            , renderError state.passwordError
            ]
        , HH.button
            [ HP.type_ HP.ButtonSubmit
            , HP.class_ (HH.ClassName "submit-button")
            , HP.disabled (not (isFormValid state))
            ]
            [ HH.text "Отправить" ]
        ]
      
    renderError :: Maybe String -> H.HTML FormAction
    renderError (Just err) = HH.div [ HP.class_ (HH.ClassName "error") ] [ HH.text err ]
    renderError Nothing = HH.text ""
    
    isFormValid :: FormState -> Boolean
    isFormValid state =
      isNothing state.usernameError &&
      isNothing state.emailError &&
      isNothing state.passwordError &&
      not (null state.username) &&
      not (null state.email) &&
      not (null state.password)
    
    handleAction :: FormAction -> H.HalogenM FormState FormAction () FormOutput IO Unit
    handleAction = case _ of
      SetUsername val ->
        H.modify_ \st -> st { username = val }
      
      SetEmail val ->
        H.modify_ \st -> st { email = val }
      
      SetPassword val ->
        H.modify_ \st -> st { password = val }
      
      ValidateUsername ->
        H.modify_ \st -> st { usernameError = validateUsername st.username }
      
      ValidateEmail ->
        H.modify_ \st -> st { emailError = validateEmail st.email }
      
      ValidatePassword ->
        H.modify_ \st -> st { passwordError = validatePassword st.password }
      
      SubmitForm -> do
        state <- H.get
        -- Здесь можно отправить данные на сервер
        H.raise $ FormSubmitted state

Примеры из практики



Теоретические знания и пошаговые инструкции — это хорошо, но настоящее понимание приходит через практический опыт. Рассмотрим несколько реальных примеров, которые демонстрируют, как GHC JS бэкенд может применяться в различных сценариях веб-разработки.

Создание диаграмм с D3.js



D3.js — мощная библиотека для создания интерактивных визуализаций данных. Совмещение её с типобезопасностью Haskell даёт интересные возможности. Реализуем простую круговую диаграмму. Сначала подключим D3 через npm:

Bash
1
npm install d3
Создадим мост между Haskell и D3:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// js/d3-bridge.js
import * as d3 from 'd3';
 
window.D3Bridge = {
  createPieChart: function(selector, data) {
    const width = 450;
    const height = 450;
    const margin = 40;
    const radius = Math.min(width, height) / 2 - margin;
 
    // Удаляем существующую диаграмму, если есть
    d3.select(selector).selectAll("*").remove();
    
    const svg = d3.select(selector)
      .append("svg")
        .attr("width", width)
        .attr("height", height)
      .append("g")
        .attr("transform", `translate(${width/2}, ${height/2})`);
    
    const color = d3.scaleOrdinal()
      .domain(data.map(d => d.name))
      .range(d3.schemeCategory10);
    
    const pie = d3.pie()
      .value(d => d.value);
    
    const data_ready = pie(data);
    
    svg.selectAll('path')
      .data(data_ready)
      .enter()
      .append('path')
      .attr('d', d3.arc()
        .innerRadius(0)
        .outerRadius(radius)
      )
      .attr('fill', d => color(d.data.name))
      .attr("stroke", "white")
      .style("stroke-width", "2px")
      .style("opacity", 0.7);
      
    svg.selectAll('text')
      .data(data_ready)
      .enter()
      .append('text')
      .text(d => d.data.name)
      .attr("transform", d => {
        const pos = d3.arc().innerRadius(radius/2).outerRadius(radius).centroid(d);
        return `translate(${pos})`;
      })
      .style("text-anchor", "middle")
      .style("font-size", 15);
      
    return true;
  }
};
Теперь напишем Haskell-код для работы с этим мостом:

Haskell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
-- src/D3Chart.hs
module D3Chart where
 
import GHC.JS.Foreign
import GHC.JS.Prim
import Data.Aeson (ToJSON, toJSON, encode)
import Data.Text.Encoding (encodeUtf8)
import qualified Data.Text as T
 
-- Модель данных для диаграммы
data ChartItem = ChartItem
  { name :: String
  , value :: Double
  } deriving (Show)
 
instance ToJSON ChartItem where
  toJSON item = object
    [ "name" .= name item
    , "value" .= value item
    ]
 
-- FFI для создания диаграммы
foreign import javascript unsafe
  "window.D3Bridge.createPieChart($1, JSON.parse($2))"
  js_createPieChart :: JSString -> JSString -> IO JSVal
 
-- Обертка для создания диаграммы
createPieChart :: String -> [ChartItem] -> IO Bool
createPieChart selector items = do
  let jsonData = T.unpack $ decodeUtf8 $ encode items
  result <- js_createPieChart (toJSString selector) (toJSString jsonData)
  return $ fromJSBool result
 
-- Пример использования
examplePieChart :: IO ()
examplePieChart = do
  let data = 
        [ ChartItem "JavaScript" 67.8
        , ChartItem "TypeScript" 25.4
        , ChartItem "CoffeeScript" 6.8
        , ChartItem "Haskell" 3.7
        , ChartItem "Elm" 2.3
        ]
  
  -- Создаем элемент для диаграммы
  createDiv "#chart-container"
  
  -- Отрисовываем диаграмму
  success <- createPieChart "#chart-container" data
  if success
    then putStrLn "Диаграмма успешно отрисована"
    else putStrLn "Ошибка при отрисовке диаграммы"
 
-- Вспомогательная функция для создания div
foreign import javascript unsafe
  "document.body.innerHTML += '<div id=' + $1.substring(1) + ' style=\"width:500px;height:500px;\"></div>'"
  createDiv :: JSString -> IO ()

Интеграция с Three.js для 3D-визуализации



Three.js — популярная библиотека для создания 3D-графики в веб-браузере. Интеграция её с Haskell позволяет создавать типобезопасные 3D-приложения. Настроим мост:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// js/three-bridge.js
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
 
window.ThreeBridge = {
  scene: null,
  camera: null,
  renderer: null,
  cube: null,
  controls: null,
  
  init: function(containerId) {
    // Удаляем существующий canvas, если есть
    const container = document.getElementById(containerId);
    if (!container) return false;
    
    container.innerHTML = '';
    
    // Создаем сцену
    this.scene = new THREE.Scene();
    this.scene.background = new THREE.Color(0xf0f0f0);
    
    // Создаем камеру
    this.camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
    this.camera.position.z = 5;
    
    // Создаем рендерер
    this.renderer = new THREE.WebGLRenderer({ antialias: true });
    this.renderer.setSize(container.clientWidth, container.clientHeight);
    container.appendChild(this.renderer.domElement);
    
    // Добавляем освещение
    const light = new THREE.DirectionalLight(0xffffff, 1);
    light.position.set(1, 1, 1);
    this.scene.add(light);
    
    // Орбитальные контролы для управления камерой
    this.controls = new OrbitControls(this.camera, this.renderer.domElement);
    
    // Запускаем анимацию
    this.animate();
    
    return true;
  },
  
  addCube: function(x, y, z, size, color) {
    const geometry = new THREE.BoxGeometry(size, size, size);
    const material = new THREE.MeshStandardMaterial({ color: color });
    const cube = new THREE.Mesh(geometry, material);
    cube.position.set(x, y, z);
    this.scene.add(cube);
    return cube.id; // Возвращаем ID для последующего управления
  },
  
  rotateCube: function(id, x, y, z) {
    const cube = this.scene.getObjectById(id);
    if (cube) {
      cube.rotation.x += x;
      cube.rotation.y += y;
      cube.rotation.z += z;
      return true;
    }
    return false;
  },
  
  animate: function() {
    requestAnimationFrame(() => this.animate());
    this.controls.update();
    this.renderer.render(this.scene, this.camera);
  }
};
Haskell-интерфейс для Three.js:

Haskell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
-- src/ThreeJS.hs
module ThreeJS where
 
import GHC.JS.Foreign
import GHC.JS.Prim
import Data.IORef
 
-- Типы для Three.js
newtype SceneId = SceneId JSVal
newtype ObjectId = ObjectId JSVal
 
-- FFI для Three.js
foreign import javascript unsafe
  "window.ThreeBridge.init($1)"
  js_initThree :: JSString -> IO JSVal
 
foreign import javascript unsafe
  "window.ThreeBridge.addCube($1, $2, $3, $4, $5)"
  js_addCube :: Double -> Double -> Double -> Double -> JSString -> IO JSVal
 
foreign import javascript unsafe
  "window.ThreeBridge.rotateCube($1, $2, $3, $4)"
  js_rotateCube :: JSVal -> Double -> Double -> Double -> IO JSVal
 
-- Обертки для типобезопасного использования
initThreeScene :: String -> IO Bool
initThreeScene containerId =
  fromJSBool <$> js_initThree (toJSString containerId)
 
addCube :: Double -> Double -> Double -> Double -> String -> IO ObjectId
addCube x y z size color =
  ObjectId <$> js_addCube x y z size (toJSString color)
 
rotateCube :: ObjectId -> Double -> Double -> Double -> IO Bool
rotateCube (ObjectId id) x y z =
  fromJSBool <$> js_rotateCube id x y z
 
-- Интерактивный пример с вращающимся кубом
example3DScene :: IO ()
example3DScene = do
  -- Создаем контейнер для сцены
  createContainer "three-container"
  
  -- Инициализируем Three.js
  success <- initThreeScene "three-container"
  
  if not success
    then putStrLn "Ошибка инициализации Three.js"
    else do
      -- Добавляем куб
      cubeId <- addCube 0 0 0 1 "#ff5533"
      
      -- Запускаем анимацию с обновлением каждые 16мс (примерно 60fps)
      animateCube cubeId
      
  where
    animateCube cubeId = do
      -- Создаем ссылку для отслеживания времени
      timeRef <- newIORef 0
      
      -- Устанавливаем функцию-колбек для анимации
      let animate = do
            time <- readIORef timeRef
            let newTime = time + 0.01
            writeIORef timeRef newTime
            
            -- Вращаем куб с разной скоростью по каждой оси
            rotateCube cubeId 0.01 0.02 0.005
            
            -- Запланировать следующий кадр
            js_requestAnimationFrame animate
      
      -- Запускаем анимацию
      callback <- syncCallback' animate
      js_requestAnimationFrame callback
 
foreign import javascript unsafe
  "document.body.innerHTML += '<div id=\"' + $1 + '\" style=\"width:600px;height:400px;\"></div>'"
  createContainer :: JSString -> IO ()
 
foreign import javascript unsafe
  "requestAnimationFrame($1)"
  js_requestAnimationFrame :: Callback (IO ()) -> IO ()

Создание SPA с маршрутизацией



Одним из важных аспектов современных веб-приложений является одностраничная архитектура (SPA) с клиентской маршрутизацией. Реализуем простой роутер для Haskell-приложения:

Haskell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
-- src/Router.hs
module Router where
 
import GHC.JS.Foreign
import GHC.JS.Prim
import Control.Monad (void)
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
 
-- | Типы для маршрутизации
type RouteHandler = IO ()
type Routes = Map String RouteHandler
 
-- | FFI для работы с History API
foreign import javascript unsafe
  "window.addEventListener('popstate', $1)"
  js_addPopStateListener :: Callback (IO ()) -> IO ()
 
foreign import javascript unsafe
  "window.history.pushState(null, '', $1)"
  js_pushState :: JSString -> IO ()
 
foreign import javascript unsafe
  "window.location.pathname"
  js_getPathname :: IO JSString
 
-- | Тип роутера
data Router = Router
  { routes :: Routes
  , notFoundHandler :: RouteHandler
  , currentRoute :: String
  }
 
-- | Создание нового роутера
newRouter :: RouteHandler -> IO Router
newRouter notFound = do
  path <- fromJSString <$> js_getPathname
  return $ Router
    { routes = Map.empty
    , notFoundHandler = notFound
    , currentRoute = path
    }
 
-- | Добавление маршрута
addRoute :: Router -> String -> RouteHandler -> Router
addRoute router path handler =
  router { routes = Map.insert path handler (routes router) }
 
-- | Запуск роутера
startRouter :: Router -> IO ()
startRouter router = do
  -- Обрабатываем текущий маршрут
  handleRoute router =<< fromJSString <$> js_getPathname
  
  -- Устанавливаем слушателя для обработки навигации по истории
  callback <- syncCallback' $ handleRoute router =<< fromJSString <$> js_getPathname
  js_addPopStateListener callback
  
  -- Перехватываем клики по ссылкам для SPA-навигации
  installLinkInterceptor router
 
-- | Обработка маршрута
handleRoute :: Router -> String -> IO ()
handleRoute router path =
  case Map.lookup path (routes router) of
    Just handler -> handler
    Nothing -> notFoundHandler router
 
-- | Программная навигация
navigateTo :: Router -> String -> IO ()
navigateTo router path = do
  js_pushState (toJSString path)
  handleRoute router path
 
-- | Перехват кликов по ссылкам для SPA-навигации
foreign import javascript unsafe
  """
  (function(callback) {
    document.body.addEventListener('click', function(e) {
      if (e.target.tagName === 'A' && 
          e.target.getAttribute('href').charAt(0) === '/' && 
          !e.ctrlKey && !e.metaKey) {
        e.preventDefault();
        const path = e.target.getAttribute('href');
        callback(path);
      }
    });
  })
  """
  js_installLinkInterceptor :: Callback (JSString -> IO ()) -> IO ()
 
installLinkInterceptor :: Router -> IO ()
installLinkInterceptor router = do
  callback <- syncCallback1' $ \pathJS -> do
    let path = fromJSString pathJS
    navigateTo router path
  js_installLinkInterceptor callback
Пример использования роутера:

Haskell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
-- Прример SPA с маршрутизацией
exampleSPA :: IO ()
exampleSPA = do
  -- Создаем базовый HTML
  createSPAContainer
  
  -- Создаем роутер с обработчиком 404
  router <- newRouter $ do
    setContent "app-container" "<h1>Страница не найдена</h1><p>Вернитесь на <a href='/'>главную</a></p>"
  
  -- Добавляем маршруты
  let router' = router
        [INLINE]addRoute[/INLINE] "/" $ do
          setContent "app-container" "<h1>Главная страница</h1><p>Добро пожаловать в наше SPA приложение на Haskell!</p><ul><li><a href='/about'>О нас</a></li><li><a href='/contact'>Контакты</a></li></ul>"
        
        [INLINE]addRoute[/INLINE] "/about" $ do
          setContent "app-container" "<h1>О нас</h1><p>Мы пишем типобезопасные веб-приложения на Haskell!</p><p><a href='/'>На главную</a></p>"
        
        [INLINE]addRoute[/INLINE] "/contact" $ do
          setContent "app-container" "<h1>Контакты</h1><p>Напишите нам на [email protected]</p><p><a href='/'>На главную</a></p>"
  
  -- Запускаем роутер
  startRouter router'
 
-- Вспомогательные функции
foreign import javascript unsafe
  "document.body.innerHTML = '<div id=\"app-container\"></div>'"
  createSPAContainer :: IO ()
 
foreign import javascript unsafe
  "document.getElementById($1).innerHTML = $2"
  setContent :: JSString -> JSString -> IO ()

Интеграция с библиотекой анимаций GSAP



GSAP (GreenSock Animation Platform) — одна из самых производительных библиотек для создания анимаций в вебе. Рассмотрим пример её интеграции с Haskell:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// js/gsap-bridge.js
import { gsap } from "gsap";
 
window.GSAPBridge = {
  animate: function(selector, duration, properties) {
    return gsap.to(selector, { 
      duration: duration,
      ...JSON.parse(properties)
    });
  },
  
  timeline: function() {
    return gsap.timeline();
  },
  
  addToTimeline: function(timeline, selector, duration, properties) {
    timeline.to(selector, { 
      duration: duration,
      ...JSON.parse(properties)
    });
    return timeline;
  },
  
  playTimeline: function(timeline) {
    timeline.play();
    return true;
  }
};
Теперь Haskell-интерфейс:

Haskell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
-- src/GSAP.hs
module GSAP where
 
import GHC.JS.Foreign
import GHC.JS.Prim
import Data.Aeson (ToJSON, toJSON, encode, object, (.=))
import Data.Text.Encoding (encodeUtf8)
import qualified Data.Text as T
 
-- Типы для GSAP
newtype Timeline = Timeline (Foreign Timeline)
newtype Animation = Animation (Foreign Animation)
 
-- Свойства анимации
data AnimProps = AnimProps
  { x :: Maybe Double
  , y :: Maybe Double
  , rotation :: Maybe Double
  , opacity :: Maybe Double
  , scale :: Maybe Double
  , backgroundColor :: Maybe String
  , ease :: Maybe String
  , delay :: Maybe Double
  , onComplete :: Maybe (IO ())
  } deriving (Show)
 
instance ToJSON AnimProps where
  toJSON props = object $ filter ((/= "null") . show . snd) $ 
    [ "x" .= x props
    , "y" .= y props
    , "rotation" .= rotation props
    , "opacity" .= opacity props
    , "scale" .= scale props
    , "backgroundColor" .= backgroundColor props
    , "ease" .= ease props
    , "delay" .= delay props
    ]
 
defaultProps :: AnimProps
defaultProps = AnimProps
  { x = Nothing
  , y = Nothing
  , rotation = Nothing
  , opacity = Nothing
  , scale = Nothing
  , backgroundColor = Nothing
  , ease = Nothing
  , delay = Nothing
  , onComplete = Nothing
  }
 
-- FFI для GSAP
foreign import javascript unsafe
  "window.GSAPBridge.animate($1, $2, $3)"
  js_animate :: JSString -> Double -> JSString -> IO Animation
 
foreign import javascript unsafe
  "window.GSAPBridge.timeline()"
  js_timeline :: IO Timeline
 
foreign import javascript unsafe
  "window.GSAPBridge.addToTimeline($1, $2, $3, $4)"
  js_addToTimeline :: Timeline -> JSString -> Double -> JSString -> IO Timeline
 
foreign import javascript unsafe
  "window.GSAPBridge.playTimeline($1)"
  js_playTimeline :: Timeline -> IO JSVal
 
-- Обёртки для типобезопасного использования
animate :: String -> Double -> AnimProps -> IO Animation
animate selector duration props = do
  let jsonProps = T.unpack $ decodeUtf8 $ encode props
  
  -- Устанавливаем колбек onComplete, если он есть
  case onComplete props of
    Just callback -> do
      cbRef <- syncCallback' callback
      js_setOnComplete (toJSString jsonProps) cbRef
    Nothing -> pure ()
  
  Animation <$> js_animate (toJSString selector) duration (toJSString jsonProps)
 
-- Вспомогательная функция для установки колбека
foreign import javascript unsafe
  """
  (function(propsJSON, callback) {
    var props = JSON.parse(propsJSON);
    props.onComplete = callback;
    return JSON.stringify(props);
  })
  """
  js_setOnComplete :: JSString -> Callback (IO ()) -> IO JSString
 
createTimeline :: IO Timeline
createTimeline = js_timeline
 
addToTimeline :: Timeline -> String -> Double -> AnimProps -> IO Timeline
addToTimeline tl selector duration props = do
  let jsonProps = T.unpack $ decodeUtf8 $ encode props
  
  -- Обрабатываем колбек аналогично animate
  propsWithCallback <- case onComplete props of
    Just callback -> do
      cbRef <- syncCallback' callback
      js_setOnComplete (toJSString jsonProps) cbRef
    Nothing -> pure $ toJSString jsonProps
  
  js_addToTimeline tl (toJSString selector) duration propsWithCallback
 
playTimeline :: Timeline -> IO Bool
playTimeline tl = fromJSBool <$> js_playTimeline tl

Сравнение подходов и альтернативные решения



Haskell для веб-разработки — не единственный вариант использования функциональных языков в браузере. Существует ряд альтернативных подходов, каждый со своими сильными и слабыми сторонами. Давайте сравним GHC JS бэкенд с другими популярными решениями.

Сравнение GHC JS бэкенда с PureScript



PureScript — это небольшой функциональный язык, вдохновленный Haskell, но изначально созданный для компиляции в JavaScript. Он имеет несколько существенных отличий от подхода GHC JS бэкенда:

Преимущества PureScript:
1. Легковесность: PureScript имеет значительно меньшую стандартную библиотеку и генерирует более компактный JavaScript.
2. Экосистема: Инструменты как spago (менеджер пакетов) и интеграция с инструментами сборки JavaScript лучше проработаны.
3. Генерация кода: PureScript создает более читаемый JavaScript-код, похожий на тот, что написал бы человек.
4. Скорость компиляции: Компилятор PureScript работает быстрее, чем GHC.

Преимущества GHC JS бэкенда:
1. Совместимость с экосистемой Haskell: Можно использовать тысячи существующих Haskell-библиотек.
2. Совместный код: Серверная и клиентская части могут использовать один и тот же код, скомпилированный для разных платформ.
3. Расширения языка: Доступны все расширения GHC, включая многострочные строки, шаблонный Haskell, generic-типизацию и другие.
4. Производительность: GHC имеет более мощные оптимизации на уровне компилятора.

Выбор между ними часто зависит от требований проекта. Если вам важны легкость и быстрая итерация, PureScript может быть лучшим выбором. Если же критична совместимость с экосистемой Haskell и общий кодбейс между сервером и клиентом, GHC JS бэкенд предпочтительнее.

Haskell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
-- Пример компонента на PureScript
module Components.Counter where
 
import Prelude
import Halogen as H
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
 
type State = { count :: Int }
 
data Action = Increment | Decrement
 
component :: forall q i o m. H.Component q i o m
component =
  H.mkComponent
    { initialState: const { count: 0 }
    , render
    , eval: H.mkEval H.defaultEval { handleAction = handleAction }
    }
  where
  render :: State -> H.ComponentHTML Action () m
  render state =
    HH.div_
      [ HH.h1_ [ HH.text "Counter" ]
      , HH.p_ [ HH.text (show state.count) ]
      , HH.button [ HE.onClick (const Increment) ] [ HH.text "+" ]
      , HH.button [ HE.onClick (const Decrement) ] [ HH.text "-" ]
      ]
 
  handleAction :: Action -> H.HalogenM State Action () o m Unit
  handleAction = case _ of
    Increment -> H.modify_ \st -> st { count = st.count + 1 }
    Decrement -> H.modify_ \st -> st { count = st.count - 1 }

Сравнение с Elm



Elm — еще один функциональный язык, специально созданный для фронтенд-разработки. Он отличается от Haskell более строгим и минималистичным подходом.

Преимущества Elm:
1. Предсказуемость: Elm гарантирует отсутствие ошибок времени выполнения, включая знаменитые "undefined is not a function".
2. Архитектура: Четко определенная The Elm Architecture (TEA) направляет разработчиков к единообразной структуре приложения.
3. Дружелюбие к новичкам: Более простая система типов и ясные сообщения об ошибках делают вход ниже.
4. Маленький размер бандла: Компилятор Elm создает очень компактный JavaScript-код.

Ограничения Elm:
1. Строгая изоляция: Взаимодействие с JavaScript требует дополнительных усилий через порты или флаги.
2. Ограниченная экспрессивность: Отсутствуют многие абстракции высшего порядка, доступные в Haskell.
3. Небольшая экосистема: Меньше библиотек по сравнению с экосистемой JavaScript или Haskell.

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
-- Пример компонента на Elm
module Main exposing (..)
 
import Browser
import Html exposing (Html, button, div, h1, p, text)
import Html.Events exposing (onClick)
 
-- MODEL
type alias Model = { count : Int }
 
init : Model
init = { count = 0 }
 
-- UPDATE
type Msg = Increment | Decrement
 
update : Msg -> Model -> Model
update msg model =
  case msg of
    Increment -> { model | count = model.count + 1 }
    Decrement -> { model | count = model.count - 1 }
 
-- VIEW
view : Model -> Html Msg
view model =
  div []
    [ h1 [] [ text "Counter" ]
    , p [] [ text (String.fromInt model.count) ]
    , button [ onClick Increment ] [ text "+" ]
    , button [ onClick Decrement ] [ text "-" ]
    ]
 
-- MAIN
main =
  Browser.sandbox
    { init = init
    , update = update
    , view = view
    }

WebAssembly как альтернативный подход



WebAssembly (WASM) — бинарный формат инструкций, который можно запускать в современных браузерах. Он предлагает альтернативный путь компиляции Haskell для веба, минуя JavaScript.

Преимущества использования WASM:
1. Производительность: WASM выполняется быстрее JavaScript, особенно для вычислительно-интенсивных задач.
2. Размер: Бинарный формат часто компактнее эквивалентного JavaScript-кода.
3. Безопасность: WASM исполняется в песочнице с четкими ограничениями памяти и средствами защиты.
4. Совместимость: Поддерживается всеми современными браузерами.

Текущие ограничения WASM для Haskell:
1. Сборка мусора: WASM пока не имеет встроенной сборки мусора, что создает сложности для языков с автоматическим управлением памятью.
2. Интеграция с DOM: Взаимодействие с DOM необходимо организовывать через JavaScript, что требует дополнительных слоев абстракции.
3. Зрелость инструментов: Инструменты для компиляции Haskell в WASM находятся в ранней стадии разработки.

Текущие эксперименты с Haskell и WASM обычно используют Asterius — компилятор, который транслирует GHC Core в WebAssembly. Однако эти проекты пока еще не достигли уровня зрелости GHC JS бэкенда.

Haskell
1
2
3
4
5
6
7
8
9
-- Пример потенциального использования Haskell с WASM
foreign import wasm "math_functions" wasmSqrt :: Double -> Double
 
computeHypotenuse :: Double -> Double -> Double
computeHypotenuse a b = wasmSqrt (a*a + b*b)
 
-- WASM-функции могут быть значительно быстрее для вычислений
performHeavyCalculation :: [Double] -> [Double]
performHeavyCalculation = map (\x -> wasmSqrt x * sin x * cos x / log (x + 1))

HASKELL
Добрый вечер, прошу помощи у знающих Haskell, не понимаю его, не для меня видимо, но сдать дисциплину надо, не хотелось бы вылететь с последнего...

Haskell и указатели
Здравствуйте! Я новенький в Haskell, заинтересовался возможностью взаимодействия Haskell с другими языками и застрял на том, как передать коду на С...

Литература па Haskell
Доброго времени суток! Решил изучить Haskell и столкнулся со следующей проблемой: для изучения функционального программирования необходимо знать...

Haskell Tuples
Добрый день, вообще не могу разобраться как сделать задачу.. Помогите пожалуйста используя unzip Есть список из tuples: нужно на выходе получить...

Haskell return
привет! main:: String -&gt; IO main:: String -&gt; IO() main:: IO()

Литература по Haskell
Посоветуйте хорошую литературу (желательно на русском).

Selenium+haskell
Здравствуйте! Появилось свободное время. Решил узнать, подойдет ли хаскелл для автотестов. По логике моего автотеста, мне надо на странице...

Template Haskell
Кто в теме, можете дать пример объявления типов и классов типов с помощью цитирующих скобок, а то я что-то туплю.

Haskell Monad
При изучении Haskell, нашёл довольно интересное задание и пробую его выполнить, но возникли определённые проблемы. Суть задания состоит в том, что...

Интеграл Haskell
Всем доброго времени суток. Помогите, пожалуйста, написать программу на Haskell: Найти интеграл функции методом правых прямоугольников. Интеграл...

Деревья в Haskell
Здравствуйте! Пишу лабораторную и возникла проблема: не получается описать 2 функции. Вот мое задание: Лексические деревья (trie-деревья)...

Сервер на Haskell
Здравствуйте. Захотелось написать небольшую серверную программу на Haskell. До этого писал только на C++ с использованием ст. библиотеки Си, а...

Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Рисование коллайдеров физического движка Box2D-WASM v3 на Three.js
8Observer8 04.06.2025
Erin Catto (автор Box2D) переписал с нуля Box2D v2 с С++ на Си и появилась версия Box2D v3. Birch-san собрал Box2D v3 в WASM, чтобы можно было использовать Box2D v3 на JavaScript. В этом примере я. . .
Worker Threads и многопоточность в Node.js
Reangularity 03.06.2025
Если вы когда-нибудь посещали собеседования на позицию Node. js разработчика, почти наверняка слышали заезженную фразу: "Node. js - однопоточная платформа". Звучит как неоспоримый факт, который. . .
Event-Driven CQRS на C# с паттерном Outbox
stackOverflow 03.06.2025
В традиционной модели происходит примерно следующее: вы получаете команду, обрабатываете ее, сохраняете результат в базу данных и затем пытаетесь опубликовать событие в брокер сообщений. Но что если. . .
OwenLogic: перенос сетевых переменных в панель Weintek (EasyBuilder Pro)
ФедосеевПавел 03.06.2025
ВВЕДЕНИЕ ПЕРЕД ЭКСПЕРИМЕНТАМИ - СОЗДАЙТЕ РЕЗЕРВНЫЕ КОПИИ ПРОЕКТОВ На момент написания статьи (02 июня 2025 г. ) самыми актуальными версиями ПО являются: OwenLogic v. 2. 10. 366 EasyBuilder Pro. . .
Dev-c++5.11 Покорение вершины
russiannick 02.06.2025
С утра преследовала одна мысль - вот бы выучить С++. Сказано-сделано. Окончив смену, скачал в интернете бестселлер Дэвиса Dev-C++ для чайников. Книга оказалась интересной и я скачал среду, на примере. . .
Тестирование Pull Request в Kubernetes с GitHub Actions и GKE
Mr. Docker 02.06.2025
Мы все знаем, что тестирование на локальной машине или в изолированном CI-окружении — это не совсем то же самое, что тестирование в реальном кластере Kubernetes. Контекстно-зависимые ошибки, проблемы. . .
Оптимизация CMake для ускорения сборки
bytestream 02.06.2025
Вы когда-нибудь ловили себя на мысле, что пока ваш проект компилируется, можно успеть сварить кофе, прочитать главу книги или даже сбегать в соседний офис? Если да, то добро пожаловать в клуб. . .
JS String.prototype.localeCo­mpare()
mr_dramm 02.06.2025
скопировано из этой темы чтобы не потерялось. localeCompare без указания локали для сравнения строк под капотом использует Intl. Collator , который работает согласно Unicode Collation Algorithm. . .
Облако проектов
russiannick 01.06.2025
Слава Джа, написал прогу для компиляции. Значит написал компилятор? Обьем кода 300+ строк. Язык-яву. Вводим данные, заполняем поля, тычем радиобаттоны. И по итогу в поле результат получам листинг. . .
Rust и квантовые вычисления: интеграция с Q# и Qiskit
golander 01.06.2025
Мир квантовых вычислений традиционно оставался закрытым клубом для высокоуровневых языков типа Python и специализированных DSL вроде Q#. Однако в последние годы Rust начал тихую революцию в этой. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru
OSZAR »