Войти
ФлеймФорумПроЭкты

Локализация лямбдами - я сделяль

#0
14:42, 12 дек 2019

Ближайшую неделю я без пк, так что трейсер стоит на месте (кажется, там GenerateLambertian на самом деле не ламбертиан).
Зато можно заняться идеей попроще.
В ходе недавнего срачика зашла речь о локализации, и у меня появилась мысль: а что, если для формирования строк использовать не просто постановку, а полноценную лямбда-машину. Внутренние имена строк - это имена функций; перевод - это тело функции, в которое подставляются эти переменные; а язык - это пространство имён с описаниями функций.
Суть подхода состоит в том, чтобы управление постановкой (например, числовые форматы) также происходило через механизм функций, и могло быть дополнительно расширено для каждого языка (например, падежными функциями для русских "штука"/"штуки"/"штук").
Запланированный дизайн таков.
При запуске программы, объект переводчика загружает из файла/ресурсов/литералов описания подстановок:

$hello = "ALLOU $user ETO TI?"
$notify = "ALLOU, ETO [$name]$supplier, [PRINES,PRINESLA][$verb]$supplier TEBE $count $product"
$bakery = [$count,BULOCHKU,BULOCHKI,BULOCHEK]$numnoun
$granny = # {
  [name=BABUSHKA]
  [verb=# $2]
  $1
}
$numnoun = # {
  [x100=[$1,100]$n-mod]
  [x10=[$1,10]$n-mod]
  [xc=[$x100,10,20]$n-within]
  [xa=[$x10,1]$n-equal]
  [xa=[$xa,[$xc]$b-not]$b-and]
  [xb=[$x10,2,4]$n-within]
  [xb=[$xb,[$xc]$b-not]$b-and]
  [$xa,$2,$xb,$3,$b-true,$4]$cases
}

Затем, чтобы получить перевод строки, программа вызывает интерпретатор и передаёт ему фактические параметры:

static auto hello = tr.Prepare("# [user=$1]$hello");
static auto notify = tr.Prepare("# [supplier=$1, count=$2, product=$3]$notify");
printf("%s\n", tr.Translate(hello, user.Name()));
printf("%s\n", tr.Translate(notify, supplier.Name(), pack.Count(), pack.Item().Name()));

Механизм тут примерно такой: [] задаёт локальные определения для подстановки, $ подставляет, "" задают строки с пробелами, без них пробелы съедаются без следа, {} окружают многострочные определения и задают область действия []. # задаёт значения для $1, $2 и т. д. из скобки, стоящей непосредственно слева.

#1
15:01, 12 дек 2019

Похоже на https://gamedev.ru/flame/forum/?id=243414, но там название лучше.

#2
5:00, 13 дек 2019

Panzerschrek[CN]
> Поэтому я выступаю за несколько иное универсальное решение - внедрить
> полноценный тьюринг-полный язык для описания локализации. Тогда каждая строка с
> параметрами будет функцией, а автор переводов будет просто писать набор этих
> функций.
Ну да, я так и хочу сделать.

Panzerschrek[CN]
> В качестве языка нужно выбрать один из широко-используемых языков (Python,
> Rust, Lua). В крайнем случае надо создать один язык, стандартизовать его и
> заставить всех его использовать.
Ого, а вот на счёт этого не уверен.

Но, в любом случае, спасибо за ссылку, поизучаю.

#3
17:32, 14 дек 2019

Итак, попытавшись действительно перейти от $$$ до BABUSHKI, я пришёл к вот такой модели выполнения.

Интерпретатор по очереди считывает токены из исходного текста. Различаются следующие виды токенов:
- Строковые литералы. Это последовательности незарезервированных символов (перечисленных далее) и esc-последовательностей.
- Кавычки "". Снаружи кавычек, пробельные символы пропускаются, тогда как внутри воспринимаются как литерал.
- Область действия {}. Задаёт область видимости для локальных объявлений, также играют роль операторных скобок.
- Заполнитель $имя.
- Маркер правила замены & и тела замены =
- Маркер лямбды @
- Заполнитель аргумента #1
- Аппликация лямбды []
- Маркер немедленного раскрытия --

Рассмотрим на примере раскрытия вот этого текста:

&hello = "ALLOU $user ETO TI?"
&notify = "ALLOU, ETO [$name]$supplier, [PRINES,PRINESLA][$verb]$supplier TEBE $count $product"
&bakery = [$count,BULOCHKU,BULOCHKI,BULOCHEK]$numnoun
&granny = @ {
  &name = BABUSHKA
  &verb = @ #2
  #1
}
&numnoun = @ {
  &x100 = --[#1,100]$n-mod
  &x10 = --[#1,10]$n-mod
  &xc = --[$x100,10,20]$n-within
  &xa = --[$x10,1]$n-equal
  &xa = --[$xav,[$xcc]$b-not]$b-and
  &xb = --[$x10,2,4]$n-within
  &xb = --[$xbv,[$xcc]$b-not]$b-and
  [$xa,#2,$xb,#3,$b-true,#4]$cases
}

[$granny, 15, $bakery] @
  &supplier = #1
  &count = #2
  &product = #3
  $notify

Основная масса текста - это &= определения, записанные в файле локализации.
Последний абзац - это исходный вид строки, сгенерированный программным кодом:

static auto notify = tr.Prepare("@ &supplier = #1 &count = #2, &product = #3]$notify");
printf("%s\n", tr.Translate(notify, "$granny", 15, "$bakery"));

Встретив [], интерпретатор готовится вызвать функцию - аргументы сохраняются на стеке, и следующим токеном ожидается тело функции.
Здесь оно записано явно, поэтому сразу поглощается маркер @, и интерпретатор приступает к раскрытию тела.
Дальше, встретив определения &=, он также запоминает их на стеке.
Аргументы и подстановки не раскрываются, если только не снабжены явным маркером --.
Таким образом, текущее состояние можно изобразить так:

&1-1 = $granny
  &1-2 = 15
  &1-3 = $bakery
  &supplier = $1-1
  &count = $1-2
  &product = $1-3
<>
  $notify

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

&1-1 = $granny
  &1-2 = 15
  &1-3 = $bakery
  &supplier = $1-1
  &count = $1-2
  &product = $1-3
<>
  "ALLOU, ETO " [$name]$supplier ", " [PRINES,PRINESLA][$verb]$supplier " TEBE " $count " " $product

Теперь впереди потока стоит литерал - переносим его сразу на выход.

"ALLOU, ETO "
<>
  &1-1 = $granny
  &1-2 = 15
  &1-3 = $bakery
  &supplier = $1-1
  &count = $1-2
  &product = $1-3
<>
  [$name]$supplier ", " [PRINES,PRINESLA][$verb]$supplier " TEBE " $count " " $product

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

"ALLOU, ETO "
<>
  &1-1 = $granny
  &1-2 = 15
  &1-3 = $bakery
  &supplier = $1-1
  &count = $1-2
  &product = $1-3
  [$name]
<>
  $supplier ", " [PRINES,PRINESLA][$verb]$supplier " TEBE " $count " " $product

"ALLOU, ETO "
<>
  &1-1 = $granny
  &1-2 = 15
  &1-3 = $bakery
  &supplier = $1-1
  &count = $1-2
  &product = $1-3
  [$name]
<>
  $1-1 ", " [PRINES,PRINESLA][$verb]$supplier " TEBE " $count " " $product

"ALLOU, ETO "
<>
  &1-1 = $granny
  &1-2 = 15
  &1-3 = $bakery
  &supplier = $1-1
  &count = $1-2
  &product = $1-3
  [$name]
<>
  $granny ", " [PRINES,PRINESLA][$verb]$supplier " TEBE " $count " " $product

"ALLOU, ETO "
<>
  &1-1 = $granny
  &1-2 = 15
  &1-3 = $bakery
  &supplier = $1-1
  &count = $1-2
  &product = $1-3
  [$name]
<>
  { @ {
    &name = BABUSHKA
    &verb = @ #2
    #1
  } } ", " [PRINES,PRINESLA][$verb]$supplier " TEBE " $count " " $product

Аргументы лямбды #1 всегда берут значение от вызова своей исходной лямбды, даже если в процессе раскрытия окажутся внутри вложенного вызова. С другой стороны, именованные заполнители $name берут значение из самого вложенного контекста, в котором окажутся.

"ALLOU, ETO "
<>
  &1-1 = $granny
  &1-2 = 15
  &1-3 = $bakery
  &supplier = $1-1
  &count = $1-2
  &product = $1-3
  {
    &2-1 = $name
<>
    &name = BABUSHKA
    &verb = @ #2
    #1
  }
  ", " [PRINES,PRINESLA][$verb]$supplier " TEBE " $count " " $product

"ALLOU, ETO "
<>
  &1-1 = $granny
  &1-2 = 15
  &1-3 = $bakery
  &supplier = $1-1
  &count = $1-2
  &product = $1-3
  {
    &2-1 = $name
    &name = "BABUSHKA"
    &verb = @ #2
<>
    $2-1
  }
  ", " [PRINES,PRINESLA][$verb]$supplier " TEBE " $count " " $product

"ALLOU, ETO "
<>
  &1-1 = $granny
  &1-2 = 15
  &1-3 = $bakery
  &supplier = $1-1
  &count = $1-2
  &product = $1-3
  {
    &2-1 = $name
    &name = "BABUSHKA"
    &verb = @ #2
<>
    $name
  }
  ", " [PRINES,PRINESLA][$verb]$supplier " TEBE " $count " " $product

"ALLOU, ETO "
<>
  &1-1 = $granny
  &1-2 = 15
  &1-3 = $bakery
  &supplier = $1-1
  &count = $1-2
  &product = $1-3
  {
    &2-1 = $name
    &name = "BABUSHKA"
    &verb = @ #2
<>
    "BABUSHKA"
  }
  ", " [PRINES,PRINESLA][$verb]$supplier " TEBE " $count " " $product

"ALLOU, ETO BABUSHKA"
<>
  &1-1 = $granny
  &1-2 = 15
  &1-3 = $bakery
  &supplier = $1-1
  &count = $1-2
  &product = $1-3
<>
  ", " [PRINES,PRINESLA][$verb]$supplier " TEBE " $count " " $product

"ALLOU, ETO BABUSHKA, "
<>
  &1-1 = $granny
  &1-2 = 15
  &1-3 = $bakery
  &supplier = $1-1
  &count = $1-2
  &product = $1-3
<>
  [PRINES,PRINESLA][$verb]$supplier " TEBE " $count " " $product

#4
17:33, 14 дек 2019

Разумеется, как и намекает название треда, функции также могут быть аргументами и возвращаемыми значениями.

"ALLOU, ETO BABUSHKA, "
<>
  &1-1 = $granny
  &1-2 = 15
  &1-3 = $bakery
  &supplier = $1-1
  &count = $1-2
  &product = $1-3
  {
    [PRINES,PRINESLA]
<>
    [$verb]$supplier
  }
  " TEBE " $count " " $product

"ALLOU, ETO BABUSHKA, "
<>
  &1-1 = $granny
  &1-2 = 15
  &1-3 = $bakery
  &supplier = $1-1
  &count = $1-2
  &product = $1-3
  {
    [PRINES,PRINESLA]
    {
      [$verb]
<>
      $supplier
    }
  }
  " TEBE " $count " " $product

"ALLOU, ETO BABUSHKA, "
<>
  &1-1 = $granny
  &1-2 = 15
  &1-3 = $bakery
  &supplier = $1-1
  &count = $1-2
  &product = $1-3
  {
    [PRINES,PRINESLA]
    {
      [$verb]
<>
      $granny
    }
  }
  " TEBE " $count " " $product

"ALLOU, ETO BABUSHKA, "
<>
  &1-1 = $granny
  &1-2 = 15
  &1-3 = $bakery
  &supplier = $1-1
  &count = $1-2
  &product = $1-3
  {
    [PRINES,PRINESLA]
    {
      &3-1 = $verb
<>
      &name = BABUSHKA
      &verb = @ #2
      #1
    }
  }
  " TEBE " $count " " $product

"ALLOU, ETO BABUSHKA, "
<>
  &1-1 = $granny
  &1-2 = 15
  &1-3 = $bakery
  &supplier = $1-1
  &count = $1-2
  &product = $1-3
  {
    [PRINES,PRINESLA]
    {
      &3-1 = $verb
      &name = BABUSHKA
      &verb = @ #2
<>
      $verb
    }
  }
  " TEBE " $count " " $product

"ALLOU, ETO BABUSHKA, "
<>
  &1-1 = $granny
  &1-2 = 15
  &1-3 = $bakery
  &supplier = $1-1
  &count = $1-2
  &product = $1-3
  {
    [PRINES,PRINESLA]
    {
      &3-1 = $verb
      &name = BABUSHKA
      &verb = @ #2
<>
      @ #2
    }
  }
  " TEBE " $count " " $product

"ALLOU, ETO BABUSHKA, "
<>
  &1-1 = $granny
  &1-2 = 15
  &1-3 = $bakery
  &supplier = $1-1
  &count = $1-2
  &product = $1-3
  {
    [PRINES,PRINESLA]
<>
    @ #2
  }
  " TEBE " $count " " $product

"ALLOU, ETO BABUSHKA, "
<>
  &1-1 = $granny
  &1-2 = 15
  &1-3 = $bakery
  &supplier = $1-1
  &count = $1-2
  &product = $1-3
  {
    &4-1 = "PRINES"
    &4-2 = "PRINESLA"
<>
    #2
  }
  " TEBE " $count " " $product

"ALLOU, ETO BABUSHKA, "
<>
  &1-1 = $granny
  &1-2 = 15
  &1-3 = $bakery
  &supplier = $1-1
  &count = $1-2
  &product = $1-3
  {
    &4-1 = "PRINES"
    &4-2 = "PRINESLA"
<>
    "PRINESLA"
  }
  " TEBE " $count " " $product

"ALLOU, ETO BABUSHKA, PRINESLA TEBE "
<>
  &1-1 = $granny
  &1-2 = 15
  &1-3 = $bakery
  &supplier = $1-1
  &count = $1-2
  &product = $1-3
<>
  $count " " $product

"ALLOU, ETO BABUSHKA, PRINESLA TEBE "
<>
  &1-1 = $granny
  &1-2 = 15
  &1-3 = $bakery
  &supplier = $1-1
  &count = $1-2
  &product = $1-3
<>
  15 " " $product

"ALLOU, ETO BABUSHKA, PRINESLA TEBE 15 "
<>
  &1-1 = $granny
  &1-2 = 15
  &1-3 = $bakery
  &supplier = $1-1
  &count = $1-2
  &product = $1-3
<>
  $product

"ALLOU, ETO BABUSHKA, PRINESLA TEBE 15 "
<>
  &1-1 = $granny
  &1-2 = 15
  &1-3 = $bakery
  &supplier = $1-1
  &count = $1-2
  &product = $1-3
<>
  $bakery

"ALLOU, ETO BABUSHKA, PRINESLA TEBE 15 "
<>
  &1-1 = $granny
  &1-2 = 15
  &1-3 = $bakery
  &supplier = $1-1
  &count = $1-2
  &product = $1-3
<>
  [$count,BULOCHKU,BULOCHKI,BULOCHEK]$numnoun

#5
17:34, 14 дек 2019

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

"ALLOU, ETO BABUSHKA, PRINESLA TEBE 15 "
<>
  &1-1 = $granny
  &1-2 = 15
  &1-3 = $bakery
  &supplier = $1-1
  &count = $1-2
  &product = $1-3
  {
    &5-1 = $count
    &5-2 = "BULOCHKU"
    &5-3 = "BULOCHKI"
    &5-4 = "BULOCHEK"
<>
    &x100 = --[#1,100]$n-mod
    &x10 = --[#1,10]$n-mod
    &xc = --[$x100,10,20]$n-within
    &xa = --[$x10,1]$n-equal
    &xa = --[$xav,[$xcc]$b-not]$b-and
    &xb = --[$x10,2,4]$n-within
    &xb = --[$xbv,[$xcc]$b-not]$b-and
    [$xa,#2,$xb,#3,$b-true,#4]$cases
  }

Здесь объявления снабжены маркером принудительного раскрытия --, значит, интерпретатор сразу вычислит значение подстановки и запомнит результат.

"ALLOU, ETO BABUSHKA, PRINESLA TEBE 15 "
<>
  &1-1 = $granny
  &1-2 = 15
  &1-3 = $bakery
  &supplier = $1-1
  &count = $1-2
  &product = $1-3
  {
    &5-1 = $count
    &5-2 = "BULOCHKU"
    &5-3 = "BULOCHKI"
    &5-4 = "BULOCHEK"
    &x100 = {
<>
      [#1,100]$n-mod
    }
    &x10 = --[#1,10]$n-mod
    &xc = --[$x100,10,20]$n-within
    &xa = --[$x10,1]$n-equal
    &xa = --[$xav,[$xcc]$b-not]$b-and
    &xb = --[$x10,2,4]$n-within
    &xb = --[$xbv,[$xcc]$b-not]$b-and
    [$xa,#2,$xb,#3,$b-true,#4]$cases
  }

"ALLOU, ETO BABUSHKA, PRINESLA TEBE 15 "
<>
  &1-1 = $granny
  &1-2 = 15
  &1-3 = $bakery
  &supplier = $1-1
  &count = $1-2
  &product = $1-3
  {
    &5-1 = $count
    &5-2 = "BULOCHKU"
    &5-3 = "BULOCHKI"
    &5-4 = "BULOCHEK"
    &x100 = {
      [#1,100]
<>
      @ <implied intrinsic for #1 % #2>
    }
    &x10 = --[#1,10]$n-mod
    &xc = --[$x100,10,20]$n-within
    &xa = --[$x10,1]$n-equal
    &xa = --[$xav,[$xcc]$b-not]$b-and
    &xb = --[$x10,2,4]$n-within
    &xb = --[$xbv,[$xcc]$b-not]$b-and
    [$xa,#2,$xb,#3,$b-true,#4]$cases
  }

"ALLOU, ETO BABUSHKA, PRINESLA TEBE 15 "
<>
  &1-1 = $granny
  &1-2 = 15
  &1-3 = $bakery
  &supplier = $1-1
  &count = $1-2
  &product = $1-3
  {
    &5-1 = $count
    &5-2 = "BULOCHKU"
    &5-3 = "BULOCHKI"
    &5-4 = "BULOCHEK"
    &x100 = {
      &6-1 = &5-1
      &6-2 = 100
<>
      <implied intrinsic for $6-1 % $6-2>
    }
    &x10 = --[#1,10]$n-mod
    &xc = --[$x100,10,20]$n-within
    &xa = --[$x10,1]$n-equal
    &xa = --[$xav,[$xcc]$b-not]$b-and
    &xb = --[$x10,2,4]$n-within
    &xb = --[$xbv,[$xcc]$b-not]$b-and
    [$xa,#2,$xb,#3,$b-true,#4]$cases
  }

"ALLOU, ETO BABUSHKA, PRINESLA TEBE 15 "
<>
  &1-1 = $granny
  &1-2 = 15
  &1-3 = $bakery
  &supplier = $1-1
  &count = $1-2
  &product = $1-3
  {
    &5-1 = $count
    &5-2 = "BULOCHKU"
    &5-3 = "BULOCHKI"
    &5-4 = "BULOCHEK"
    &x100 = {
      &6-1 = &5-1
      &6-2 = 100
<>
      <implied intrinsic for 15 % 100>
    }
    &x10 = --[#1,10]$n-mod
    &xc = --[$x100,10,20]$n-within
    &xa = --[$x10,1]$n-equal
    &xa = --[$xav,[$xcc]$b-not]$b-and
    &xb = --[$x10,2,4]$n-within
    &xb = --[$xbv,[$xcc]$b-not]$b-and
    [$xa,#2,$xb,#3,$b-true,#4]$cases
  }

"ALLOU, ETO BABUSHKA, PRINESLA TEBE 15 "
<>
  &1-1 = $granny
  &1-2 = 15
  &1-3 = $bakery
  &supplier = $1-1
  &count = $1-2
  &product = $1-3
  {
    &5-1 = $count
    &5-2 = "BULOCHKU"
    &5-3 = "BULOCHKI"
    &5-4 = "BULOCHEK"
    &x100 = 15
<>
    &x10 = --[#1,10]$n-mod
    &xc = --[$x100,10,20]$n-within
    &xa = --[$x10,1]$n-equal
    &xa = --[$xav,[$xcc]$b-not]$b-and
    &xb = --[$x10,2,4]$n-within
    &xb = --[$xbv,[$xcc]$b-not]$b-and
    [$xa,#2,$xb,#3,$b-true,#4]$cases
  }

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

"ALLOU, ETO BABUSHKA, PRINESLA TEBE 15 "
<>
  &1-1 = $granny
  &1-2 = 15
  &1-3 = $bakery
  &supplier = $1-1
  &count = $1-2
  &product = $1-3
  {
    &5-1 = $count
    &5-2 = "BULOCHKU"
    &5-3 = "BULOCHKI"
    &5-4 = "BULOCHEK"
    &x100 = 15
    &x10 = 5
    &xc = <intrinsic true>
    &xa = <intrinsic false>
    &xa = <intrinsic false>
    &xb = <intrinsic false>
    &xb = <intrinsic false>
<>
    [$xa,#2,$xb,#3,$b-true,#4]$cases
  }

"ALLOU, ETO BABUSHKA, PRINESLA TEBE 15 "
<>
  &1-1 = $granny
  &1-2 = 15
  &1-3 = $bakery
  &supplier = $1-1
  &count = $1-2
  &product = $1-3
  {
    &5-1 = $count
    &5-2 = "BULOCHKU"
    &5-3 = "BULOCHKI"
    &5-4 = "BULOCHEK"
    &x100 = 15
    &x10 = 5
    &xc = <intrinsic true>
    &xa = <intrinsic false>
    &xa = <intrinsic false>
    &xb = <intrinsic false>
    &xb = <intrinsic false>
<>
    "BULOCHEK"
  }

"ALLOU, ETO BABUSHKA, PRINESLA TEBE 15 BULOCHEK"
<>
<>

#6
21:41, 15 дек 2019

Решил тем временем поизучать аналоги.
Когда-то давно упоминали нечто по имени TRAC, как функциональный тексто-ориентированный язык. Посмотрел. Интересно... но не то.
В частности, там нет тривиального способа сделать аналог моего $granny - что, по сути, объект с полями в системе типов, где объектов нет.
Вроде, ещё TeX обладал достаточно навороченной системой раскрытия макросов, надо будет изучить его.

#7
22:43, 15 дек 2019

Итак, в TeX я тоже ничего похожего не нашёл (хотя, может, плохо искал).
Таким образом, единственный аналог концепции - это, пожалуй, Wolfram в целом и ReplaceAll+Function в частности.
Походу, моя комбинация lambda- и let-выражений - это что-то не совсем обычное для современных программистов. Прикольно. Тем интереснее реализовать и посмотреть на практике.

#8
10:35, 18 дек 2019

Пруф концепции: https://rextester.com/LRR95820
Впечатления? Отзывы? Код ревью? Политика?

Прошло более 2 лет
#9
3:03, 10 мар 2022

Имбирная Ведьмочка
> В частности, там нет тривиального способа сделать аналог моего $granny - что,
> по сути, объект с полями в системе типов, где объектов нет.

| $sup-granny = @ {
  | $name = бабушка
  | $verb = @ #2
  #1
}

| $prod-bakery = [$count,булочку,булочки,булочек]$numnoun

| $notify = "Аллоу, это [$name]$supplier, [принёс,принесла][$verb]$supplier тебе $count $product"

Или, на более привычном языке:

def sup_granny(k, **context):
    return k(name = "бабушка", verb = (lambda k1, k2: k2), **context)

def prod_bakery(**context):
    return numnoun(context["count"], "булочку", "булочки", "булочек")

def notify(**context):
    return (
        "Аллоу, это " +
        context["supplier"](lambda **ct: ct["name"]) +
        ", " +
        context["supplier"](lambda **ct: ct["verb"]("принёс", "принесла")) +
        " тебе " +
        str(context["count"]) +
        " " +
        context["product"](**context)
    )

Раз уж вспомнили, комментарий 3 года спустя.
То, что тут происходит - это Mogensen–Scott encoding - как раз тот самый костыль, через который и представляют объекты данных в языке, где вместо объектов - только функции (к которым относится и чистое лямбда-исчисление).

ФлеймФорумПроЭкты

Тема в архиве.