Личный веб-сервер на Wolfram Language

    Иногда людям хочется быстро сделать веб-сервер, корневая логика которого будет на 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
    • 0
      Каждый запуск скрипта ведь заново запускает wolfram kernel и занимает очень много времени?
      • 0
        Верно. Причем при генерации картинки оно еще и фронтенд запускает, судя по всему. Сделал кое-какие замеры:

        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
      • +3
        Эмм, то есть получается, что на Wolfram Language написан не сам сервер, а лишь своего рода cgi-скрипт. А сервер написан на питоне
        • +2
          Строго говоря, да. Я не нашел какого-либо прямого способа работы с сокетами в Wolfram Language. Вообще, при написании этой статьи была робкая надежда, что где-то в комментариях объявится кто-то, кто скажет «Да как так можно-то? Вот как эту задачу нужно было решить на самом деле».

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

            Это всё или есть еще что-то интересное? Есть ли что-то, что принципиально невозможно/сложно сделать в другом языке, вроде python или даже ruby?
            • –1
              Вам тоже советую мой пост пятиминутной давности: Wolfram Language (Mathematica) — это просто игрушка. Сила WL в функциональном программировании, которое взято из таких языков как Lisp (на него WL больше всего похож), Haskell и ML.
              Эти самые функции можно удобно между собой комбинировать — вывод одной можно направить на вход другой.

              На bash посмотрите.
              • +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
                Поищите в документации к WL запуск функций из библиотек на других языках. Если есть — то берём любую библиотеку, в которой есть возможность запуска TCP-сервера и готово. Далее, попробуйте поискать, есть ли WL возможность использовать BSD sockets или возможность запускать функции из POSIX API (если всё это происходит на UNIX, разумеется). Далее, в WL есть возможность компиляции функций (функция Compile). Компилировать можно двумя способами: либо в специальное внутреннее представление Mathematica, либо в специальный сгенерированный код на C. Попробуйте поискать, можно ли вызывать из WL функции C, в том числе из этого самого кода на C, выдаваемого Compile. И вообще, раз можно из WL запускать сишный код, выданный Compile, значит, должна быть и возможность запустить произвольный сишный код. Далее, собственно, повызывайте сишные функции из BSD sockets
                  • 0
                    exit = LibraryFunctionLoad["/lib/x86_64-linux-gnu/libc.so.6", "exit", {Integer}, Integer]
                    exit[0]
                    

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

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

                    Боже мой, у Стивена Хьюговича наверняка портрет с собой есть
                    • +1
                      На Mac OS, например, было бы так:
                      WOLFRAM_EXECUTABLE = "/Applications/Mathematica.app/Contents/MacOS/MathKernel"

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