Небольшой интерпретируемый функциональный язык программирования, написанный на F#. Примеры программ - в папке examples, краткая документация - ниже.
Весь проект написан на платформе .NET версии .NET 6.0. Для сборки проекта необходимо:
- Установить .NET SDK с поддержкой версии .NET 6.0.
- Собрать проект либо в Visual Studio, открыв файл ppl/ppl.sln, либо через CLI, прописав в директории ppl команду
dotnet build
.
Для запуска программы, на языке pepelang
необходимо запустить собранный бинарник и прописать первым аргументом командной строки
путь к файлу с исходным кодом:
Пример:
bin\Debug\net6.0\ppl.exe ../examples/helloworld.ppl
Далее - краткое введение в язык.
Программы на языке состоят из списка выражений, которые вычисляются по одному по очереди. Каждое выражение заканчивается символом ;
. Программа должна содержать хотя бы
одно выражение. Программа на языке записывается в одном исходном файле с расширением .ppl
. Программы могут вызывать программы из других файлов с помощью конструкции import
.
Пример:
import some-name;
Данная конструкция сначала ищет в стандартном наборе библиотек, есть ли библиотека под названием "some-name", и если нет, то ищет файл с названием "some-name.ppl" в папке с файлом, в котором был написана конструкция. Если такой файл существует, программа из него выполняется и всё окружение в исполненной программе, передается в текущую. Можно считать, что когда мы вызываем конструкцию import, он просто выполняет программу из другого файла внутри нашей.
Язык pepelang - функциональный, поэтому программы в основном состоят из применения различных функций. Общий синтаксис применения функции выглядит так:
{<func-name> <arg1> <arg2> ... <argn>};
Пример:
{square 2}; // 4
{sum 3 5}; // 8
В языке так же есть операторы - это те же функции, но с особыми именами - их имена могут состоять только из символов из набора "+-=*&|!></~"
Пример:
{+ 5 6}; // 11
Для определения переменных, функций и операторов используется конструкция let
и let-in
.
Синтаксис Let выглядит так:
let <name> <arg1> <arg2> ... <argn> = <expr>;
Без аргументов конструкция let
просто присваивает имени name
результат выражения expr
.
При указании аргументов, выражение автоматически оборачивается в функцию от указанных аргументов, то есть к примеру запись
let twice x = {+ x x};
объявляет функцию twice
, которая прибавляет то, что ей подали к самому себе.
Стоит отметить, что все функции и операторы, объявленные таким способом являются каррированными, то есть запись
let sum x y = {+ x y};
создает функцию sum, которая является функцией от одного аргумента, возвращающая функцию от одного аргумента, которая в свою очередь
уже считает сумму двух чисел.
Таким же образом можно объявить оператор - для этого надо использовать в имени оператора только особые символы.
После того, как мы прописали конструкцию let
, она создает именованную переменную, которая будет жить всё оставшееся время программы. Когда нам
необходимо создать временную переменную или функцию, стоит использовать конструкцию let-in
. Вот ее общий вид:
let <name> <arg1> <arg2> ... <argn> = <expr1> in <expr2>;
Работает так же как и let
, но создает лишь временную переменную, вычисляет с ее помощью значение expr2
и удаляет переменную name
из окружения.
В языке есть следующие базовые типы данных:
- int - знаковое 32-битное целое число.
12;
-14;
- string - строка
"Hello world!";
- bool - булевые тип
true;
false;
- none - тип с одним значением None
None;
- float - вещественный тип данных
1.64;
0.23;
13.;
.43;
- tuple - кортеж из других(любых) выражений
(1, 2);
(1.023, "Meow");
((2, 3), (1, 2, 3, (1.02, 13) ), None);
- literal - вспомогательный тип данных. По сути представляет собой строку без пробелов и табуляций. В коде начинает с символа
%
%nl;
%some_string;
Лямбда функции определяются следующим образом:
\<var-name> -> <expr>;
Пример:
\x -> {+ x 2};
\x -> \y -> {+ x y};
В языке pepelang
функции могут принимать аргументы любого типа и возвращать любой тип, однако иногда возникает надобность проверки объектов на соответствие какому-то типу, для этого в языке есть
ограничения типов, они могут использоваться либо в функции std.match_type
, либо в сопоставлении с образцом, в конструкции match
(про них дальше).
Чтобы объявить ограничение типа используется ключевое слово type
.
Чтобы обозначить, что на каком то месте может стоять объект либо одного типа, либо другого используется конструкция choice
- choice <type1> | <type2> | <type3> ...
.
Чтобы обозначить, что на каком то месте может стоять любой объект, используется специальный символ _
.
Примеры:
type IntOrString = choice int | string;
type TupleOf3 = (_, _, _);
type Vec2 = (float, float);
type Mat2x2 = (Vec2, Vec2);
type Complex = (IntOrString | Vec2, TupleOf3, (_, _));
Сопоставление с образцом выглядит следующим образом:
match <exp> with
| <exp1> -> <exp1_>
| <exp2> -> <exp2_>
...
| <expn> -> <expn_> $;
Где слева от стрелок стоят образцы сопоставления, а справа - выражения для вычисления. Выражение exp
по очереди сравнивается с каждым образцом и при совпадении вычисляет соответствующее выражение
для вычисления.
Примеры:
match x with
| _x of int -> "int"
| n of None -> "none"
| (x of choice int | float, y of choice int | float) -> "num tuple2"
| _ -> "something other" $;
В языке помимо обычных функций и операторов, представляющих из себя выражения, в которые можно подставить значения переменных, чтобы вычислить их значение, присутствут так же "внутренние" функции и операторы.
Это особый вид выражения, который внутри себя содержит функцию F#, которая принимает на вход список выражений и возвращает выражение. В коде на языке pepelang
невозможно определить
внутреннюю функцию, это можно сделать только в коде на F# и добавить ее в составе отдельной библиотеки в программу.
При применении внутренние функции и операторы ведут себя не так, как обычные. Внутренние функции и операторы не каррированные - при применении их к списку аргументов, функция получает их в виде списка.
Стандартная библиотека подключается в каждый файл с исходным кодом. Она содержит некоторые функции и операторы для работы с базовыми типами данных.
В стандартной библиотеке находятся следующие операторы:
+, -, *, /, <, >, <=, >=, =, !=, ||, &&, !, >>
>>
- оператор композиции функций.
std.mod
- остаток от деления. Принимает два аргумента.
{std.mod 5 3}; // 2
std.match_type
- соответсвует ли выражение типу или ограничению типа. Принимает два аргумента.
{std.match_type 5 int}; // true
{std.match_type (2, 3.0) (int, float)}; // true
{std.match_type (2, (None) ) (int, float)}; // false
std.print
- вывод выражений. Принимает произвольное количество аргументов. Если на вход поступает литерал%nl
выводит символ перехода на новую строку.
{std.print "hello world!" %nl};
{std.print (1, 2, 3) %nl "meow" %nl};
std.read_line
- считывает строку из потока ввода. Игнорирует все аргументы.
let s = {std.read_line};
std.to_int
- конвертирует выражение в число. Принимает один аргумент.
{std.to_int "35"}; // 35
{std.to_int 35.4}; // 35
std.to_float
- конвертирует выражение в вещественное число. Принимает один аргумент.
{std.to_float "35.4"}; // 35.4
std.parse
- парсит строку и пытается конвертировать ее в выражение из базовых типов. Принимает один аргумент - строку.
{std.parse "3"}; // 3
{std.parse "(1, 2, (3.45, None) )"}; // (1, 2, (3.45, None))
std.id
- функция, которая возвращает первый ее аргумент.
{std.id 3}; // 3
let fact n = if {= n 0} then 1 else {* n {fact {- n 1} } };
{std.print "Enter n:"};
let n = {std.parse {std.read_line} };
if {std.match_type n int} then
{std.print {fact n} %nl}
else
{std.print "Incorrect input" %nl };
Больше примеров в папке examples. Там - реализация списков, двоичных деревьев поиска и линз.