SoftCraft
разноликое программирование

Top.Mail.Ru

Проект "Логика"


А.И.Орехов
© 2006 г.

Статья о проекте нового языка программирования высокого уровня.

В Интернете кипят споры. Что лучше? Си или Delphi, Java или Ruby. Противопоставления самих языков и концепций, лежащих в их основе. Некая "псевдорелигиозность" присуща как начинающим программистам, так и светилам кибернетики. Доступно множество серьезных аналитических статей, где доказывается не состоятельность и преимущества тех или иных подходов.

Тем временем все больше новых языков сочетает разные парадигмы. Ведь что-то лучше программировать в функциональном стиле, а другие задачи предпочтительно решать процедурно ООП. Где то важна гибкость, а где то быстродействие. Какое место займет новый язык программирования.

Разработка предназначается для создания инженерных и научных программ, не требующих высокого быстродействия. Развитые средства функционального программирования, которые действуют подобно ООП. И четкая иерархическая структура, позволяющая легко читать исходник. Предпосылкой создания является тот факт, что мало современных разработок в области инженерных задач, которым полезна выразительность и краткость записи, но не требуется высокая вычислительная мощность. Скорость написания программы, ее лаконичность, мощные семантич. возможности приоритетны. Сейчас идет разработка языка, подробная информация на сайте www.logica.net.ru. Выход альфа версии транслятора под .NET предварительно намечен к осени 2007 года.

Иерархическая структура

"Логика" основана на подходах процедурного программирования и ООП, ФП и элементах ЛП. Но они не смешиваются друг с другом, а находятся в иерархической взаимосвязи. Условно можно выделить три слоя языка:
1 слой: ООП и процедурное программирование, классы и объекты;
2 слой: функциональное программирование и объекты-значения;
3 слой: логическое программирование в ограничениях, запросы

Из методов (слой 1) доступны все структуры языка, можно вызывать любые функции, запросы и т.п.. В функциях (слой 2) можно применять только объекты ФП и ЛП. Из запросов (слой 3) можно вызывать только конструкции логического программирования. Т.е. из 1го уровня можно вызывать все что угодно. Из 2го уровня доступен только 2ой и 3ий уровень. Из 3го уровня - только 3ий слой языка.

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

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


Синтаксис

class - класс, определение императивного объекта
proc - процедура, метод класса
proc static - статич. метод класса (может вызываться без создания экземпляра)
func - определение функции, аппликативного объекта
value - определение значения
query - запрос, логическое программирование

Значащие отступы (через табуляцию), аналогично Python. Но допустимы только символы табуляции, не пробелы.
Переменных нет в понимании Си или Паскаль. Есть имена, связываемые с объектом. У имен нет типов.
Продолжение строки '_'. Длинные строки можно разбивать на неск. физических. При этом пробелы и табуляция игнорируется.

Структура файла и компиляция

Программа на Логике состоит из файлов с расширением *.logica. Это не модули. Транслятор объединяет все файлы в один и компилирует его. Т.е. глобальное пространство имен у всех файлов общее. Поскольку язык динамический, лучше разбивать сложный проект на несколько независимо компилируемых сборок. А не делать сложные конструкции с пространствами имен в одном модуле, как позволяет C#. "Логика" ориентирована на небольшие быстро компилируемые сборки, которые можно подключать к основной программе.

Каждый отдельный файл может содержать заговолок и определения объектов: классы (class), функции (func), значения (value) и запросы (query). Сначала идет секция import, в ней перечисляются используемые сборки-библиотеки. Затем секция constante. В Логике нет глобальных объектов и переменных, но есть глобальные константы.

import
    System
    System.Console

constante
    pi = 3.1415
    str_test = 'тест'

Процедуры содержатся только в классах, свободных процедур нет. В программе должен быть класс App с определенным методом int Main(list string args). С вызова этого метода начинается выполнение программы.

class App
    proc static int Main(list string args)
        System.Console.WriteLine('Добрый день :-)')
        0

Переменные

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

int a = 3;
a = 5;
a = 7;

В ФП и декларативном программировании это не возможно по смыслу.

x = 3
x = 7 Ошибка! 3 = 7
Имя 'x' связано с объектом число 3. Теперь x это обозначение для 3, синоним. Логически не верно сравнивать его с другим объектом. Это называется неизменяемость. Подход способствует более стройному программированию и уменьшает кол-во ошибок. Но часто требуются именно ячейки памяти, для задач ввода/вывода. Либо просто по смыслу алгоритм более эффективен с ячейками. В Логике можно создавать такие переменными через ключевое слово var. Их можно использовать в классах и процедурах.
class MyClass
    var float x

    proc test()
        x = 5
        x = x + 10
        x = 7

        var float y
        y = 2*x
        y = 0

Списки

Основная встроенная структура данных - список. Элементами могут быть любые значения, объекты. Оптимизирован доступ по индексу. Сама структура декларативна, все составляющие должны быть заданы в момент создания списка. После этого длину уже невозможно изменить. Создание списка: [ ]

lst_a = [3, 3.14, "Добрый день"]

r1 = lst_a[0]
r2 = lst_a[2]

Список можно использовать в процедурах как массив, если в нем элементы var.

a = [var 3, var 7, var 'hi']
b = a[2]
a[2] = 'test'
a[2] = 3.14

Для описания фильтров со списками введено ключевое слово list.

list
list int
list bool|string

[float, int, int] # дополнительная форма фильтра без list


Декларативность

Структура данных декларативна. Но элементами списка могут быть недекларативные объекты: экземпляры class и переменные var. Это важно для разделения императивного программирования (процедуры) и функционального (func). Если список содержит только декл. элементы, то его можно свободно передавать в аргументах func. В противном случае данный список можно будет использовать только в процедурах.

Проверить объект можно с помощью встроенных функций:
bool imperative(object) - истина, если объект недекларативен
bool applicative(object) - истина, если объект декл. ( applicative(x) = not imperative(x) )

Функции в Логике

Функции в языке несут некоторые черты объектно-ориентированного программирования.
Подход: Функция это неупорядоченный список выражений.

func myfunc1(a, b)
    t = 2*k + s
    #где:
    k = 10*a + 3.14
    s = 2*b

    t/2

Функция состоит из именованных выражений. Порядок не важен, транслятор сам анализирует как выполнять код. Последнее выражение без имени - результат.

func myfunc2(a, b): myfunc1
    k1 = 4 * 3.14
    s = 4*b + k1

Наследование функций. Мы описали новую функцию func2, которая наследуется от func1. Изменили поведение функции, переопределив (перегрузка) выражение s и добавили новое k1.

func myfunc1(a, b)
    t = 2*k + s
    #где:
    k = 10*a + 3.14
    s = 2*b

    t/2
for operations
    func add(myfunc1 j):
        ...
...

В секции for operations можно указать ассоциированные обработчики для определенных операций (сложить, вычесть, умножить и т.п.).
В Логике принят оригинальный подход, который привносит некоторые особенности ООП в ФП. Исходный текст получается короче и лучше для восприятия, чем ООП с классами и объектами.

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

Частично выполняемые функции

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

Что вернет функция. Если заданы все аргументы и данных достаточно, то функция вычислит результат. Если же аргументов не достаточно, то будет создан новый объект - частично выполненная функция в качестве результата. Функция есть объект первого порядка, поэтому может быть полноценным возвращаемым значением наравне с числами, строками и т.п..

Существует два способа указывать аргументы: позиционно, без указания имен; и именовано, не зависимо от позиции.

func float f(float a, float b, float c)
    k = 3*a + 1
    s = 8*b
    (k + s) * c


f(2, 5.5, 7) # аргументы заданы, результат

f1 = f(2, 5.5) # частичное выполнение
f1(7) # новый объект-функция

f2 = f(,, 5.5)
f3 = f(2, , 7) # аргументы в любых сочетаниях

# именованные аргументы

f(b = 5.5)

# смешанный стиль, позиционные вначале

f(2, c=7)
f(, 5.5, c=7)

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

#( Функция сплайн по эксперимент. снятым точкам
xy - список эксперимент. снятых точек;
xmin, xmax - границы диапазона;
x - нужное значение для вычисления y
)#

func spline1(list point xy, xmin, xmax, x)
    # Сначала надо вычислить коэффициенты сплайн и
    # Создать новый список точек, равномерный по всему
    # диапазону
    ...
    xy_norm = ...

    # Теперь можно выполнить запрос на значение x:
    y = некая сплайн функция от x по вычисленным коэф. и_
        данным xy_norm

    y
    
for operations
    func spline1(x) add(spline1(x) j)
        #( находим пересечение диапазонов xmin и xmax
            xmin_new, xmax_new;
            вычисляем новый список xy_norm_new
            и нужные данные
        )#
        ...
        xmin_new, xmax_new = ...
        xy_norm_new = ...
        
        ...
        spline1(-, xmin_new, xmax_new, , xy_norm = xy_norm_new)

В инженерной и научной деятельности часто бывает нужно обрабатывать экспериментально снятые координаты графиков. Можно создать удобный объект-функцию, которая получит на вход произвольный список точек (x,y) и найдет необходимые промежуточные значения при помощи аппроксимации или сплайн. Наша функция создает внутреннюю структуру, равномерный список координат.

Экспериментальные данные могут быть произвольными, неравномерными. (к примеру [x, y] = [-10, 5], [-5, 4], [-3.5, 3.8], [0, 9.3], [4, 7], [10, 3.1]) Объект функция должна произвести вычисления и построить новый список, равномерный по оси x ([-10, -9, -8...]) с нужным шагом. После этого легко получить значение в любом месте по сплайн или др. формуле.

f = spline1(data, -10, 10) # получили нашу функцию с аппроксимацией

a = f(7.5) # может применять как обычную функцию в любом месте программы
b = f(0)

Также бывает нужно, сочетать несколько экспериментальных функций. К примеру акустический прибор состоит из микрофона (график характеристики), усилителя и фильтров, телефона (график характеристик). Графики микрофона и телефона снимаем экспериментально, усилитель моделируем математически. Нам надо "сложить" эти графики. Обработчик add легко решает эту задачу.

fun_mk = spline1(datamk, 0, 8000)
fun_tel = spline1(datatel, 0, 8000)
fun_res = fun_mk + fun_tel
...
res = fun_res(x)

Развитые методы функционального программирования позволяют легко и компактно решать многие инженерные задачи. Обратим внимание на эффективность. Реализации языков через замыкания (карринг) имитируют частич. вып. внешне, синтаксически. Но реально накапливают аргументы и потом полностью выполняют функцию. Представьте, что функции акустического прибора записаны в цикле по частоте (Гц). Реализация через замыкания работала бы очень медленно, поскольку все функции повторяли бы вычисления для каждой итерации, в том числе полный расчет выровненного массива и коэффициентов сплайна. В Логике же будут производиться только необходимые вычисления. Функции fun_mk и fun_tel создадут данные сплайна и массив заранее. В цикле же будет вызываться fun_res - оптимизированная, без лишней работы.

Фильтры типов

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

Фильтр можно указать в следующих случаях: аргументы функции или метода; возвращаемое значение; связывание имени (переменной). Основной принцип фильтров - то что не указано явно может быть любого типа.

func myfunc1(a, b) # аргументы и результат могут быть произвольными

# аргументы могут быть только целыми числами для a,
# и дробными для b

func myfunc1(int a, float b)

# первый аргумент может быть либо целочисленным,
# либо строкой; второй - список целых и дробных чисел.

func myfunc1(int|string a, list int|float b)

func int|float myfunc1(a, b) # аналогично проверка на тип результата

Вертикальная черта '|' служит для записи альтернативных вариантов. Можно создавать сложные фильтры со скобками.

(list int|float) | (list string) a = выражение

Выражение a может быть списком целых и дробных чисел или списком строк.

Для списков существует еще один вид фильтра:

func [float, float, string] myfunc(int a, int b)
Это удобно для небольших списков фиксированной длины.

Расширенная нотация для функций

Функции (func) могут быть частично выполненными, поэтому указать только имя не достаточно.

func f1(a, b, c)

f1 x = выражение

Выражение x должно быть функцией f1(a, b, c). Если нас интересует не полная функция, а частично выполненная.

f1(b, c) x = выражение

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

Полиморфизм функций

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

func f2(int|float a)
    2*a

func f2(string a)
    a.len()

func f2(list int|float a)
    map(lambda x,y: x+y, a, 0)

...

f2(string a) res = выражение

Выражение должно быть второй функцией, принимающей строки.

Функции и значения

Объекты языка связаны следующим образом.

Все объекты программы наследуются от корневого элемента object. Ветвь class - императивные объекты с изменением состояния, классы. От value наследуются аппликативные объекты - значения. Они полностью соответствуют ФП концепции. Функции являются частным, расширенным случаем значений.

Объектно-ориентированное и процедурное программирование в Логике мало отличается от ООП в распространенных языках, таких как Python или C#. Подробнее рассмотрим различие value и function.

value

Это базовая ветвь для func. Можно сказать, что значение (value) это функция, которую нельзя вызвать. Все встроенные типы (int, float, string, bool и т.п.) являются значениями. Можно определять свои, дополнительные типы.

value complex(float r, float j)
for operations
    func complex add(complex x)
        complex(r+x.r, j+x.j)

    ...
    func complex mul(complex x)
        complex((r * x.r) - (j * x.j), (r * x.j) + (j * x.r))

    func complex mul(int|float x)
        complex(r*x, j*x)

...

a = complex(2, 4.5) + complex(7, -1)
j = complex(0, 1)

b = 5 * j - a

В приведенном выше примере объекта-функции применен список экспериментальных точек list point.

value point(float x, float y)

Все! Такого определения уже достаточно для того, чтобы создавать экземпляры point и обращаться к ним.

p1 = point(7, 15.5)
x = p1.x
y = p1.y

Также как и функции значения можно наследовать.


Отличия value от func

Значения (value) нельзя вызвать, у них нет тела и возвращаемого результата.
Значения обязательно получают все аргументы при создании (потому что они не могут быть частично выполненными).
К аргументам value можно получать доступ через точку после создания (только на чтение, они неизменяемы). Аргументы и выражения функции доступны только через наследование и секцию for operations.

Приведение типов

Все типы в Логике (и встроенные тоже) являются объектами value. Автоматические преобразования значений из одного типа в другой запускает механизм фильтрации типов. Но программист должен описать в соответствующих функциях: как происходит преобразование и создание нового значения.

Синтаксически для этого есть два способа.

value point(int x, int y) # тип-значение: точка на плоскости
for operations
    func string string() # обработчик преобразования типа
        'point: %1, %2' % (x, y)

...
p = point(3, 5)
s = p.string()
или
string s = p # фильтр вызовет нужный метод

Мы определили новый тип значений - точка на плоскости. Для ее создания надо указать координаты x и y. Нам нужно, чтобы объект мог преобразовывать себя в строковый вид. Функция string() задает, как выполняется преобразование.

value point(int x, int y)
for operations
    conv to string()
        'point: %1, %2' % (x, y)

Более удобная синтаксическая форма для функций преобразования типов.

Создавая новый тип мы может "научить его превращаться" в любые другие типы. Но как быть, если нужно наоборот. Некий тип преобразовать в наш. К примеру: из string создать point. Для этого надо добавить соответствующую функцию в for operations значения value string. Но это может оказаться невозможно, если нет прямого доступа к исходникам этого объекта. К примеру тип создается в скомпилированном библиотечном модуле dll. Или встроенный тип. Чтобы решить эту проблему в Логику добавлена конструкция create.

value point(int x, int y)
create
    from string s
        point(..., ...)
for operations
    conv to string()
        'point: %1, %2' % (x, y)

Теперь point может свободно преобразовываться в string и обратно. На самом деле функции из секции create просто добавляются в методы соответствующего объекта-значения.

value string(String s)
for operations
    conv to point()
         point(..., ...)

Приведение типов выполняется автоматически механизмом фильтров или явно. В любом случае это функции conv to.

2 синтаксических способа:
1. conv to нашего объекта. Как объект будет "превращаться" в нечто другое.
2. create from добавляет функцию в другой тип. Как тот объект будет "превращаться" в наш. Это удобно, когда исходник типа не доступен напрямую

p1 = point(3, 7)
p2 = point(10, 10)

s = p1.string() + ", " + p2.string() # явно
string s2 = p1 # автоматически

func string str(j) # можно и так
    j.string()

s3 = str(a) + ", " + str(b)

ЛП и запросы

В язык встроены элементы логического программирования, а именно программирование в ограничениях. Пример синтаксиса:

query list myquery1(int j)
    x = 2 * y
    y >= 2 and y <= 7
    x = j or x = j + 1

    one [x, y] # или all [x, y]



Продолжнение следует... :-)