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

Я начал активно изучать Mathematica и Wolfram Language где-то полгода назад и сразу возникло желание использовать его как “повседневный” язык для разных бытовых и околорабочих задач. Знаете, у каждого есть язык, который первым приходит на ум, если нужно, скажем, проанализировать какую-то коллекцию данных или связать друг с другом несколько систем. Обычно это какой-то достаточно высокоуровневый скриптовый язык. В моем случае в этой роли выступал Python, но тут у него появился серьезный конкурент.

Однако не все можно решить, запустив блокнот Mathematica и разово выполнив код из него. Некоторые задачи требуют периодического исполнения либо запуска по какому-то событию. Нужен сервер. Для начала посмотрим, какие варианты развертывания и исполнения предлагает сама компания. Насколько я могу судить, опции следующие:
1) Старый добрый Mathematica Notebook. Иными словами, разовая рабочая сессия в GUI.
2) Wolfram Cloud. И это замечательная опция, которую использую в том числе и я. Однако есть масса причин, по которым вариант с облаком может не подойти. Назову лишь одну из них — каждый вызов стоит ненулевое количество денег. Для множества мелких периодических операций это может быть неоправданно затратно, особенно когда под рукой есть простаивающие мощности.
3) Wolfram Private Cloud. Звучит как какая-то грядущая возможность запустить собственное облако. Подробности мне неизвестны.
4) Использовать Wolfram Symbolic Transfer Protocol. Выглядит как самый основательный и универсальный способ интеграции Wolfram Language в вашу систему. Сервер здесь — лишь один из частных случаев применения. Тот самый “правильный и долгий путь”.
5) Wolfram Script. Все просто — вызываем код на Wolfram Language как любой другой скрипт, без непосредственного участия графического интерфейса. Cron, pipeline и все остальные замечательные механизмы в нашем распоряжении. Этот способ мы и используем для быстрого создания сервера.

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

import tornado.ioloop
import tornado.web
import os, subprocess
import json

WOLFRAM_EXECUTABLE = "wolfram"

def execute(arguments):
	def run_program(arguments):
	    p = subprocess.Popen(arguments,
	                         stdout=subprocess.PIPE,
	                         stderr=subprocess.PIPE)
	    return iter(p.stdout.readline, b'')
    res = ''
    for line in run_program(arguments):
        res+=line
    return res

class MainHandler(tornado.web.RequestHandler):
    def get(self):         
            out = execute([WOLFRAM_EXECUTABLE,"-script", "main.m",
                                       str(self.request.method),
                                       str(json.dumps(self.request.arguments)),
                                       str(json.dumps(self.request.headers)),
                                       str(self.request.body)])
            self.write(out)

application = tornado.web.Application([
    (r"/", MainHandler),
])

 application.listen(8888)


Собственно, “main.m” — это и есть наш скрипт на Wolfram Language. В нем нам нужно получить и интерпретировать переданные аргументы, а также вернуть результат.

method = $CommandLine[[4]]
arguments = Association @ ImportString[$CommandLine[[5]], "JSON"]
headers = Association @ ImportString[$CommandLine[[6]], "JSON"]
body = If[Length[$CommandLine] >= 7,$CommandLine[[7]], ""]

Print["Hello world"]


Наш скрипт выводит “Hello world”. Часть на питоне, в свою очередь, честно возвращает эти данные клиенту.
В принципе, в этом вся суть метода.

В таком виде наш сервер сможет принимать и возвращать только строковые данные с кодом результата 200. Хочется немного больше гибкости. Для этого данные из скрипта должны передаваться не просто в виде строки, а в каком-то структурированном виде. Так у нас появляется еще одно преобразование в JSON и обратно. Формат будет таким:

{
     “code”: 200,
     “reason”: OK,
     “body”: “Hello world"
}


Теперь его нужно корректно обработать на другой стороне.

outJson =  json.loads(out)
        self.set_status(outJson["code"], outJson["reason"])
        if(outJson["body"] != None):
            self.write(str(outJson["body"]))


Следующим шагом будет добавление возможности возвращать не только текст, но и другие данные. Возможно, два двойных преобразования JSON казались кому-то недостаточно медленным решением… Добавим в наш JSON поля “file” и “contentType”. Если поле “file” непустое, то вместо записи в поток вывода содержимого поля “body” мы считываем указанный файл.

outJson =  json.loads(out)
        self.set_status(outJson["code"], outJson["reason"])
        if(outJson["file"] != None):
            self.add_header("Content-Type", outJson["contentType"])
            with open(outJson["file"], 'rb') as f:
                while True:
                    data = f.read(16384)
                    if not data: break
                    self.write(data)
            self.finish()
            os.remove(outJson["file"])
        elif(outJson["body"] != None):
            self.write(str(outJson["body"]))


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

AsJson[input_] := ExportString[Normal @ input, "JSON"]

HTTPOut[code_, body_, reason_] := 
   <|"code"->code, "body"->body, "reason"->reason, "file"->Null|>

HTTPOutFile[expression_, exportType_, contentType_] := 
    Module[{filePath = FileNameJoin[{$TemporaryDirectory, "httpOutFile"}]},
    Export[filePath, expression, exportType];
    <|"code"->200, 
    "body"->Null, 
    "reason"->Null, 
    "file"->filePath, 
    "contentType"->contentType|>
]


Наконец, напишем обработчики конкретных методов.

HTTPGet[arguments_, headers_] := AsJson[...]

Switch[method, 
    "GET", HTTPGet[arguments, headers], 
    "POST", HTTPPost[arguments, headers, body]]


Таким образом, появляются методы HTTPGet, HTTPost и аналогичные. Настало время для создания бизнес-логики. Можно создать обработчики для различных путей (“/“, “/SomeEndpoint” и т.д.), но вместо этого мы добавим к вызову аргумент, который будет определять вызываемую функцию: “/?op=MyFunction”.
Осталось только добавить логику выбора и вызова этой функции в нашем скрипте. Используем ToExpression[].

HTTPGet[arguments_, headers_] := 
   Module[{methodName = "GET"<>arguments["op"]},
       AsJson[ToExpression[methodName][arguments, headers]]
   ]


Теперь можно просто добавить функцию GETMyFuction и первая единица бизнес-логики готова. Пусть эта функция выводит текущее время:

GETMyFuction[arguments_, headers_] := 
   HTTPOut[ToString[Now]]


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

GETTestGraph[___] := 
   Module[{},
      out = Graph[{a -> e, a -> c, b -> c, a -> d, b->d, c->a}];
      HTTPOutFile[out, "PNG", "image/png"]
   ]


Теперь, при открытии в браузере “.../?op=TestGraph” можно увидеть вот такую картинку:

image

На этом всё и удачного дня!

Комментарии (13)


  1. hidoba
    19.07.2015 15:00

    Каждый запуск скрипта ведь заново запускает wolfram kernel и занимает очень много времени?


    1. Nilis Автор
      19.07.2015 17:50

      Верно. Причем при генерации картинки оно еще и фронтенд запускает, судя по всему. Сделал кое-какие замеры:

      MacBook Pro Retina Mid 2014:

      op=Now 0.290 s
      op=TestGraph 1.5 s

      Raspberry Pi Model 2:
      op=Now 3.4 s
      op=TestGraph 7.6 s


  1. safinaskar
    19.07.2015 15:02
    +3

    Эмм, то есть получается, что на Wolfram Language написан не сам сервер, а лишь своего рода cgi-скрипт. А сервер написан на питоне


    1. Nilis Автор
      19.07.2015 19:07
      +2

      Строго говоря, да. Я не нашел какого-либо прямого способа работы с сокетами в Wolfram Language. Вообще, при написании этой статьи была робкая надежда, что где-то в комментариях объявится кто-то, кто скажет «Да как так можно-то? Вот как эту задачу нужно было решить на самом деле».

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


      1. Kroid
        19.07.2015 20:58

        Кстати говоря, раз уж вы начали говорить о «силе WL», не расскажете, в чем именно его сила, что отличает его от других языков общего назначения? Когда я с ним игрался (может, год назад), я увидел 2(3) вещи:
        1. Куча встроенных функций на все случаи жизни
        2. Эти самые функции можно удобно между собой комбинировать — вывод одной можно направить на вход другой.
        возможно, 3. одна и та же функция может принимать аргументы разных типов

        Это всё или есть еще что-то интересное? Есть ли что-то, что принципиально невозможно/сложно сделать в другом языке, вроде python или даже ruby?


        1. safinaskar
          19.07.2015 23:52
          -1

          Вам тоже советую мой пост пятиминутной давности: Wolfram Language (Mathematica) — это просто игрушка. Сила WL в функциональном программировании, которое взято из таких языков как Lisp (на него WL больше всего похож), Haskell и ML.

          Эти самые функции можно удобно между собой комбинировать — вывод одной можно направить на вход другой.

          На bash посмотрите.


        1. Nilis Автор
          20.07.2015 00:42
          +1

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

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

          Когда вы объявляете функцию, скажем, f[x_,y_]:=x+y, эти нижние прочерки там неспроста. Они означают паттерны, которые мы преобразовываем (это нечто большее, чем концепция аргументов функции). Можно задавать дополнительные условия для этих самых входных паттернов. Рассмотрим пример:

          MyPlus[x_, y_] := x + y /; x > y
          MyPlus[2, 3] := "4, lol"
          
          MyPlus[6, 5]
          > 11
          MyPlus[5, 6]
          > MyPlus[5, 6]
          MyPlus[2, 3]
          > "4, lol"
          


          Здесь у нас функция, которая складывает числа, но почему-то только если первое больше второго. В противном случае вся конструкция (»вызов функции") остается неизменной и в таком виде используется дальше. А в случае сложения 2 и 3 она вообще говорит неправду. Иными словами, я задал несколько правил преобразования символа MyPlus[_,_] а Математика по мере выполнения выбирает, какое из этих правил использовать, с приоритетом у самого специфичного правила (это не зависит от порядка объявления).

          И еще одно — при разработке программы в т.н. блокнотах происходит что-то, что отличается от обычного “изменил код — запустил — проверил”. Код и результаты его работы смешаны вместе в одном интерфейсе (таким образом, который вы зададите). Например, вам нужно модифицировать какую-то функцию — вы можете изменить и выполнить только ее, без перезапуска всей программы, видя результат ее работы прямо там же. При этом будет использован текущий “контекст” (заданные переменные и т.д.). Таким образом, граница между разработкой, исполнением и отладкой стирается, обратная связь мгновенна. Традиционный способ разработки кажется при этом уже не таким удобным.

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


      1. safinaskar
        19.07.2015 23:10
        -1

        Поищите в документации к WL запуск функций из библиотек на других языках. Если есть — то берём любую библиотеку, в которой есть возможность запуска TCP-сервера и готово. Далее, попробуйте поискать, есть ли WL возможность использовать BSD sockets или возможность запускать функции из POSIX API (если всё это происходит на UNIX, разумеется). Далее, в WL есть возможность компиляции функций (функция Compile). Компилировать можно двумя способами: либо в специальное внутреннее представление Mathematica, либо в специальный сгенерированный код на C. Попробуйте поискать, можно ли вызывать из WL функции C, в том числе из этого самого кода на C, выдаваемого Compile. И вообще, раз можно из WL запускать сишный код, выданный Compile, значит, должна быть и возможность запустить произвольный сишный код. Далее, собственно, повызывайте сишные функции из BSD sockets



      1. safinaskar
        24.07.2015 17:05

        exit = LibraryFunctionLoad["/lib/x86_64-linux-gnu/libc.so.6", "exit", {Integer}, Integer]
        exit[0]
        

        Вот таким способом можно вызывать сишные функции из WL (у меня в Mathematica 10 работает). Правда, остальные функции чё-то не работают (sqrt, open, abs и т. д.), но там, наверное, можно разобраться. Итак, этим способом можно вызывать функции socket, connect и т. д., можно вызывать всякие функции из curl и т. д.


  1. YourChief
    19.07.2015 21:11

  1. Alexeyco
    20.07.2015 10:47
    +1

    > WOLFRAM_EXECUTABLE = «wolfram»

    Ехал Вольфрам через Вольфрам…
    Видит Вольфрам: в Вольфраме Вольфрам,
    Сунул Вольфрам в Вольфрам Вольфрам
    Вольфрам Вольфрам Вольфрам Вольфрам

    Боже мой, у Стивена Хьюговича наверняка портрет с собой есть


    1. Nilis Автор
      20.07.2015 10:53
      +1

      На Mac OS, например, было бы так:
      WOLFRAM_EXECUTABLE = "/Applications/Mathematica.app/Contents/MacOS/MathKernel"