LINUX.ORG.RU

Муки выбора языка программирования

 , , , ,


2

4

Пытаюсь выбрать язык программирования для личного проекта.

Хочется, чтобы у языка были:

  • библиотека для загрузки/выгрузки изображений с поддержкой широкого круга форматов
  • биндинги для sdl2
  • работа с битовыми массивами размером больше чем 64 элемента (с поиском единиц)
  • перегрузка оператора индекса в том числе при присвоении
  • ассоциативные массивы с лаконичным доступом к элементам
  • документацией с поддержкой мобильного просмотра в 2023 году-то
  • поддержкой компиляции для мобильных архитектур
  • нормальный полиморфизм, а не как в Rust
  • востребованность на рынке труда

Всем хорош D. Он мне очень симпатичен и я все языки сравниваю с ним. Но у него настолько низкий уровень популярности, что нет нормальной библиотеки для изображений.

C++ и Rust имеют очень странные конструкторы для битовых массивов. Может это проблема документации, но я с ходу не нашёл как мне создать битовый массив из готового байтового массива, чтобы каждый байт превратился в 8 бит.

Haskell имеет поддержку даже многомерных битовых массивов, но вот документацию на мобильном листать не удобно. В принципе не критично, но я не уверен что haskell вообще подходящий инструмент для моей задачи. А задачу мою можно найти по тегу «гексагональный пиксель» здесь.

Что выбрать?

★★★★★

Ответ на: комментарий от monk

Вот расскажите про Haskell поподробнее.

Бывают задачи которые приходится писать в императивном стиле даже на Haskell и выгоды нет?

Или это всё чисто проблемы головы не перестроившейся на функциональное программирование?

unDEFER ★★★★★
() автор топика
Ответ на: комментарий от unDEFER

Бывают задачи которые приходится писать в императивном стиле даже на Haskell и выгоды нет?

По мне, даже в императивном стиле Haskell удобнее.

Во-первых, есть https://hackage.haskell.org/package/polysemy, что позволяет легко делать тесты для IO. И вообще оторвать логику программы от ввода-вывода (который может быть консольный, сетевой, графический, …).

Во-вторых, runExceptT позволяет ловить ошибки, при этом видно, какая строка может вызвать ошибку.

В-третьих, IO обычная монада, поэтому с ней можно работать почти как со списком через sequence.

В-четвёртых, система типов никуда не девается и в императивном стиле, что позволяет гарантировать, что не пропустил какую-нибудь проверку.

В-пятых, даже в императивном стиле можно лаконично записать алгоритм, например как

process context templateFile outFile =
    readFile templateFile
        >>= foldM (processLine processTemplate) context . lines
        >>= writeFile outFile . unlines . reverse . result

При этом ленивость гарантирует, что чтение и обработка файла будет идти построчно. Среди других языков такое видел только в shell и perl.

monk ★★★★★
()
Ответ на: комментарий от monk

При этом ленивость гарантирует, что чтение и обработка файла будет идти построчно. Среди других языков такое видел только в shell и perl.

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

aiive
()
Ответ на: комментарий от aiive

Но эта-же ленивость может выйти боком, т.к. к моменту, когда ты попытаешься использовать результат вычисления файл будет закрыт и получишь ошибку.

Это как??? Если читаешь через readFile, то он закроется только когда его читать перестанешь.

Если пытаться через hGetLine, результат будет сразу, так как в IO операция обязана закончиться до следующей.

monk ★★★★★
()
Ответ на: комментарий от monk

Вот пример кода, который компилируется но падает в рантайме.

module Main where

import System.IO (openFile, IOMode(ReadMode), hClose)
import qualified Data.ByteString.Lazy as BL

main :: IO ()
main = do
  h <- openFile "./test.pdf" ReadMode
  bs <- BL.hGetContents h
  hClose h
  putStrLn $ show $ BL.length bs

Если убрать hClose h то все сработает нормально

aiive
()
Ответ на: комментарий от monk

А зачем? Оно надо только если результат надо в функциональный блок передать. А так и в IO живётся неплохо.

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

aiive
()
Ответ на: комментарий от hateyoufeel

При том, что код с UB может работать, а потом переставать это делать очень внезапно при минорном обновлении компилятора. И ты об этом узнаешь по внезапным кордампам.

ну да. код с программной ошибкой может работать… и при обновлении компилятора - упасть. но корректный код не будет падать с корректным компилятором. пишите корректный код и все.

вы что сказать-то хотели?

alysnix ★★★
()
Ответ на: комментарий от alysnix

Ага. Я это ещё в прошлом сраче про UB писал: в одном только C список UB на 13 страниц в стандарте. А C++ только добавляет. «Просто пишите корректный код» тут немного не работает, потому что нужно иметь мозг с планету чтобы постоянно в голове держать все случаи. Вдобавок, нет нормальных способов узнать, есть ли в коде UB или нет. Даже UBSan далеко не всё ловит.

hateyoufeel ★★★★★
()
Ответ на: комментарий от monk

При этом ленивость гарантирует, что чтение и обработка файла будет идти построчно

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

alysnix ★★★
()
Ответ на: комментарий от monk

В-четвёртых, система типов никуда не девается и в императивном стиле, что позволяет гарантировать, что не пропустил какую-нибудь проверку.

Не, не позволяет. Только если кто-то эту проверку сделал частью типа, чем базовый хачкел обычно не может похвастаться. А когда дело доходит до насильной проверки через типы, хочется рвать волосы на жопе, потому что система типов хачкелла слишком ограничена. Посмотри на те же синглтоны: чтобы ими пользоваться, надо быть хрен знает кем. Я как только вижу код на синглтонах, обычно отодвигаю клавиатуру и иду пить кофе, задаваясь вопросом: а правда мне нужен хачкел или, может, пойти забухать?

Отдельно замечу, что Lazy I/O в хачкелле – частый источник багов. Всякие conduit, iteratee и прочие pipe не от хорошей жизни придумали.

hateyoufeel ★★★★★
()
Последнее исправление: hateyoufeel (всего исправлений: 2)
Ответ на: комментарий от aiive

import qualified Data.ByteString.Lazy as BL

Не, ну если так извращаться, то да. Я уж думал, стандартная ленивость где-то ломается.

Если очень-очень постараться, то через FFI можно и UB сотворить.

monk ★★★★★
()
Ответ на: комментарий от monk

Значит должен вывести 5.

пазор! не надо писать неоднозначные примеры. у вас cout - глобальный поток вывода, и не видно в каком состоянии и каком формате он находится. даже если он находится в десятичном, что неочевидно, то f() может поменять формат и выводить он вам будет не то, что вы ждете.

и при этом программа будет совершенно корректной, без единой ошибки.

пишите примеры без множественных толкований плиз

alysnix ★★★
()
Ответ на: комментарий от monk

Не, ну если так извращаться, то да. Я уж думал, стандартная ленивость где-то ломается.

Если ты в его коде прочитаешь String вместо Lazy ByteString, он точно так же упадёт. Lazy ByteString ленив в плане работы с самим массивом, потому что там внутри пачка чанков вместо одного большого массива. К ленивости чтения из файла это всё отношения не имеет. В его примере bs – это просто thunk, который не вычислится, пока он никому не нужен.

hateyoufeel ★★★★★
()
Последнее исправление: hateyoufeel (всего исправлений: 2)
Ответ на: комментарий от monk

Не совсем понимаю почему байтстринги - извращение. У меня был бинарный файл под рукой поэтому я использовал их.

Вот пример со стандартными строками

module Main where

import System.IO (openFile, IOMode(ReadMode), hClose, hGetContents)

main :: IO ()
main = do
  h <- openFile "./test.txt" ReadMode
  s <- hGetContents h
  hClose h
  putStrLn $ show $ length s

точно так-же будет падать.

aiive
()
Ответ на: комментарий от hateyoufeel

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

Вот это как раз от языка не зависит. Но если в нормальном языке можно написать

(ignore-errors
  (setf r (something-unsafe)))

и иметь гарантию, что something-unsafe может изменить только глобальные переменные, то в языке с UB любой код может испортить что-угодно. Более того, ошибка в функции может заставить выполниться другую функцию, которая ниоткуда не вызывается.

monk ★★★★★
()
Ответ на: комментарий от monk

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

вы наверняка намекаете, что в плюсах, если не включен нужный варнинг(а он включенным быть обязан) можно из функции выйти без возврата результата? так следовало толковать эти несчастные 42 вместо 5? если да, то просто включите варнинг и считайте это невинной шалостью.

alysnix ★★★
()
Ответ на: комментарий от aiive

Вообще неожиданно.

Once a semi-closed handle becomes closed, the contents of the associated list becomes fixed. The contents of this final list is only partially specified: it will contain at least all the items of the stream that were evaluated prior to the handle becoming closed.

Я бы в худшем случае ожидал неполное чтение. Ладно, на эти грабли я ещё не наступал.

monk ★★★★★
()
Ответ на: комментарий от alysnix

вы наверняка намекаете, что в плюсах, если не включен нужный варнинг(а он включенным быть обязан) можно из функции выйти без возврата результата? так следовало толковать эти несчастные 42 вместо 5?

Я намекаю, что при любом UB внутри f компилятор вправе поменять значение x на 42. И большая часть UB варнингами не ловится.

monk ★★★★★
()
Ответ на: комментарий от hateyoufeel

К ленивости чтения из файла это всё отношения не имеет. В его примере bs – это просто thunk, который не вычислится, пока он никому не нужен.

По идее, если заменить

bs <- hGetContents h

на

bs <- hGetContents' h

должно перестать падать.

monk ★★★★★
()
Ответ на: комментарий от monk

Я намекаю, что при любом UB внутри f компилятор вправе поменять значение x на 42. И большая часть UB варнингами не ловится.

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

alysnix ★★★
()
Ответ на: комментарий от alysnix

Я пытаюсь указать, что если в нормальных языках я могу завернуть f() в некий try/except и иметь гарантию, что она ничего не испортит, то в языках с UB любая ошибка влияет на всю программу и изолировать её невозможно. Более того, она, как правило, ещё и будет проявляться только при компиляции не в отладочном режиме.

То есть, если в нормальном языке я могу выделить некое небольшое ядро, которое гарантированно работает и, при необходимости, перезапускает другие части программы и обрабатывает исключительные ситуации (например, если программа обрабатывает некий файл, то можно гарантировать, что в случае программной ошибки файл будет записан). В программах с UB для этого надо гарантировать, что во всём выполняемом коде нет UB (условно, функция разбиения строки на слова может при неудачном стечении обстоятельств испортить файлы на диске).

monk ★★★★★
()
Ответ на: комментарий от monk

должно перестать падать.

В программах с UB для этого надо гарантировать, что во всём выполняемом коде нет UB (условно, функция разбиения строки на слова может при неудачном стечении обстоятельств испортить файлы на диске).

короче хаскел тоже не подходит… :)

alysnix ★★★
()
Ответ на: комментарий от monk

Скорее перерасход памяти, пока данные или не вычислишь или не выкинешь.

это все изза ленивости небось. читать на хаселе не умею, потому просто беру за рабочую версию.

alysnix ★★★
()
Ответ на: комментарий от monk

Добро при правильном использовании.

Но тут возникает вопрос - что лучше? Строгий по умолчанию язык с возможностью некоторые вещи делать ленивыми или ленивый по умолчанию язык с возможностью некоторые вещи сделать строгими.

aiive
()
Ответ на: комментарий от monk

Там не совсем утечки. Скорее перерасход памяти, пока данные или не вычислишь или не выкинешь.

Суть проблемы это не меняет. По мере работы программы она ест все больше и больше памяти. И иногда локализовать место, где это происходит - очень сложно.

aiive
()
Ответ на: комментарий от alysnix

мы же пытаемся выяснить источник самих UB и что это такое

Если действительно хочешь разобраться для себя, то вот неплохая статья:
https://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html
https://blog.llvm.org/2011/05/what-every-c-programmer-should-know_14.html
https://blog.llvm.org/2011/05/what-every-c-programmer-should-know_21.html

Manhunt ★★★★★
()
Последнее исправление: Manhunt (всего исправлений: 1)
Ответ на: комментарий от aiive

Добро при правильном использовании.

Но тут возникает вопрос - что лучше? Строгий по умолчанию язык с возможностью некоторые вещи делать ленивыми или ленивый по умолчанию язык с возможностью некоторые вещи сделать строгими.

Главная фишка ленивости в оптимизациях, которые она даёт и которые могут быть далеко не совсем очевидны при ручном её использовании. Так что тут шило на мыло.

hateyoufeel ★★★★★
()
Ответ на: комментарий от hateyoufeel

Главная фишка ленивости в оптимизациях,

тогда наверное ленивый хаскел всегда обгонит неленивые плюсы?.. а вот и нет. никогда не обгонит. значит тут что-то не так.

alysnix ★★★
()
Ответ на: комментарий от alysnix

Главная фишка ленивости в оптимизациях,

тогда наверное ленивый хаскел всегда обгонит неленивые плюсы?.. а вот и нет. никогда не обгонит. значит тут что-то не так.

Ага. В том, что кроме ленивости у хачкелла ещё есть рантайм, сборщик мусора и т.д.

На хачкелле можно писать код, сравнимый по производительности с сишным, но выглядеть он будет настолько чудовищно, что проще написать то же на C и дёргать через FFI. Что в общем-то все и делают.

hateyoufeel ★★★★★
()
Последнее исправление: hateyoufeel (всего исправлений: 1)
Ответ на: комментарий от alysnix

короче хаскел тоже не подходит… :)

В хаскеле если ошибка в bs, то она не может испортить произвольную другую переменную. UB в языке нет. Просто надо учитывать, что список (и вообще любая структура) там не список, а функция.

monk ★★★★★
()
Ответ на: комментарий от alysnix

Да. Ленивость заставляет хранить вычисления, а не вычисленные значения. Если вычисленное значение занимает значительно меньше места, то получается «утечка».

monk ★★★★★
()
Ответ на: комментарий от aiive

Именно для семантики Хаскела лучше ленивый по умолчанию.

Ленивость + чистота позволяют обеспечить ссылочную прозрачность и бесконечные списки, а также более простое описание многих алгоритмов.

monk ★★★★★
()
Ответ на: комментарий от alysnix

тогда наверное ленивый хаскел всегда обгонит неленивые плюсы?.. а вот и нет. никогда не обгонит. значит тут что-то не так.

Для равного алгоритма обгонит.

process context templateFile outFile =
    readFile templateFile
        >>= foldM (processLine processTemplate) context . lines
        >>= writeFile outFile . unlines . reverse . result

работает быстрее, чем

void process(Context & context, File & templateFile, File & outFile)
{
  auto s = readFile(templateFile)
  auto r1 = foldM(processLine(processTemplate), context, lines(s));
  writeFile(outFile, unlines(reverse(result(r1))));  
}

processTemplate – это функция

Context & processTemplate(Context & context, std::string & s);
monk ★★★★★
()
Ответ на: комментарий от hateyoufeel
size_t lev_dist(const std::string& s1, const std::string& s2)
{
  const auto m = s1.size();
  const auto n = s2.size();

  std::vector<int64_t> v0;
  v0.resize(n + 1);
  std::iota(v0.begin(), v0.end(), 0);

  auto v1 = v0;

  for (size_t i = 0; i < m; ++i)
  {
    v1[0] = i + 1;

    for (size_t j = 0; j < n; ++j)
    {
      auto delCost = v0[j + 1] + 1;
      auto insCost = v1[j] + 1;
      auto substCost = s1[i] == s2[j] ? v0[j] : (v0[j] + 1);

      v1[j + 1] = std::min({ delCost, insCost, substCost });
    }

    std::swap(v0, v1);
  }

  return v0[n];
}

переписывается в

{-# LANGUAGE Strict #-}
{-# OPTIONS_GHC -fllvm #-}

import qualified Data.Array.Base as A(unsafeRead, unsafeWrite)
import qualified Data.Array.ST as A
import qualified Data.ByteString as BS
import qualified Data.ByteString.Unsafe as BS
import Control.Monad.ST

levenshteinDistance :: BS.ByteString -> BS.ByteString -> Int
levenshteinDistance s1 s2 = runST $ do
  v0Init <- A.newListArray (0, n) [0..]
  v1Init <- A.newArray_ (0, n)
  loop 0 v0Init v1Init
  A.unsafeRead (if even m then v0Init else v1Init) n

  where
    m = BS.length s1
    n = BS.length s2

    loop :: Int -> A.STUArray s Int Int -> A.STUArray s Int Int -> ST s ()
    loop i v0 v1 | i == m = pure ()
                 | otherwise = do
      A.unsafeWrite v1 0 (i + 1)
      let s1char = s1 `BS.unsafeIndex` i
      let go j prev | j == n = pure ()
                    | otherwise = do
            delCost <- v0 `A.unsafeRead` (j + 1)
            substCostBase <- v0 `A.unsafeRead` j
            let substCost = if s1char == s2 `BS.unsafeIndex` j then 0 else 1
            let res = min (substCost + substCostBase) $ 1 + min delCost prev
            A.unsafeWrite v1 (j + 1) res
            go (j + 1) res
      go 0 (i + 1)
      loop (i + 1) v1 v0

И работает на 40% быстрее.

monk ★★★★★
()