Поиск стиля программирования

В книге есть главы, посвященные вопросам, про то, как написать простейшую программу и запустить ее, как работать в REPL и другое. Здесь же в качестве интересного начала, я хочу дать вам возможность сложить первое впечатление о коде, который вы будете писать или изучать. Насколько удобно в Julia писать и читать код?

Поэкспериментируем. Например, напишем простой фрагмент кода, обрабатывающий небольшой текст, якобы из html-страницы, и находящийся в переменной text.

text = """Как программировать.
       Пример программы:
       <code>
       f(x)=x+1 # объявление функции
       </code>
       """
  1. Функцией split мы разобьем текст на фрагменты по списку разделителей с помощью регулярного выраженияr"[\w\.\:\#\<\>\/\n\s]+".
  2. Каждый из полученных фрагментов строк (слов) мы проверим на наличие гласных кириллических букв, отфильтровав список по этому признаку. Для этого используем filter и ismatch.
  3. Оставшиеся слова приведем к нижнему регистру (lowercase).
  4. И, наконец, функцией join склеим слова снова в одну строку, разделив их символом "|".

Императивный стиль

Можно результат каждой операции присваивать временной переменной.

words = split(text,r"[\w\.\:\#\<\>\/\n\s]+")
words = filter(words) do word
    ismatch(r"[аяоеуюиы]",word)
end
words = map(lowercase, words)
newtext = join(words,"|")

#"как|программировать|пример|программы|объявление|функции"

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

Функциональный стиль: вложенные скобки

Если вкладывать вызовы друг в друга, как матрешки, получается сложночитаемый (как и сложносоставляемый) код:

join(
     map(lowercase,
         filter( split( text, r"[\w\.\:\#\<\>\/\n\s]+")) do word
              ismatch(r"[аяоеуюиы]",word)
         end
     ),
"|")

# "как|программировать|пример|программы|объявление|функции"

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

Функциональный стиль: использование оператора |>

Оператор |> позволяет вычисленное слева от него выражение передать аргументом в правое, которое должно быть функцией принимающей один аргумент. Обратите внимание, что синтаксис позволяет делать перенос строки после |>. Запись справа от |> должна быть или именем функции или ссылкой на нее (что почти одно и тоже) , т.е. записывается без скобок, присутствие которых означало бы вызов функции, а не ее имя. Если функция, все-таки, записана со скобками, то она и будет сначала вызвана. В этом случае, то что она вернет, в свою очередь, должно быть ссылой на функцию, принимающую один аргумент.

hascyrillic(s::AbstractString) = ismatch(r"[аяоеуюиыАЯОЕУЮЫИ]",s)

split(text, r"[\w\.\:\#\<\>\/\n\s]+") |>
               words->filter(hascyrillic, words) |>
                   words->map(lowercase, words) |>
                       words->join(words, "|")


"как|программировать|пример|программы|объявление|функции"

Этот код можно читать слева-направо, сверху-вниз.

Он вынуждает активно пользоваться синтаксисом определения анонимных функций words->join(words, "|").

Cмотрите, как hascyrillicкрасиво вписывается в качестве агрумента filter(), а lowercase - для map(). В Julia вы можете работать с функциями, как с любыми другими значениями.

Можно уменьшить шум, используя для любых параметров анонимных функций имя _ - такое имя разрешено в языке:

hascyrillic(s::AbstractString) = ismatch(r"[аяоеуюиыАЯОЕУЮЫИ]",s)

split(text, r"[\w\.\:\#\<\>\/\n\s]+") |>
               _->filter(hascyrillic, _) |>
                   _->map(lowercase, _) |>
                       _->join(_, "|")


"как|программировать|пример|программы|объявление|функции"

Функциональный стиль: объявление функций-помощников.

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

joinit(sep) = xs->join(xs, sep)
splitit(sep) = str->split(str,sep)
filterit(f::Function) = xs->filter(f,xs)
mapit(f::Function) = xs->map(f,xs)

text |>
    splitit(r"[\w\.\:\#\<\>\/\n\s]+") |>
    filterit(hascyrillic) |>
    mapit(lowercase) |>
    joinit("|")


# "как|программировать|пример|программы|объявление|функции"

Теперь код читается не хуже, чем код в императивном стиле, но лишен некоторых его недостатков.
Обратите внимание: перенос строки в коде можно делать не до, а после оператора |> иначе будет синтаксическая ошибка.

Локальные привязки let

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

s1 = "code 345"
m = match(r"(\d+)", s1)
rv = if m!=nothing
           parse(Int,m[1])
       else 0 end

Нам пришлось создать временную переменную m.
Но, во-первых, если где-то в коде выше уже была определена переменная m, то мы нечаянно перепишем ее значение. Во-вторых, где-то в коде ниже мы можем случайно ее использовать. Хотелось бы граничить область действия временной переменной. Это можно сделать с помощью конструкции let:

s1 = "code 345"
let m = match(r"(\d+)", s1)
   rv = if m!=nothing
              parse(Int,m[1])
        else 0 end
end

После let можно иметь ноль или больше локальных привязок значений, разделенных запятой, если их несколько:

m = match(r"(\d+)", s1)

Все такие переменные будут хранить присвоенное значение до конца блока end
Если переменная с таким именем не существовала - она будет создана, а потом удалена. Если переменная существовала - она временно будет затенена: оригинальная переменная станет недоступна, а ее "копия" с тем же именем будет иметь новое значение. После конца блока станет доступна оригинальная переменная с прежним значением.

Кроме того, let вводит "мягкую" локальную область видимости. Это значит: если внутри блока let ... end появятся другие привязки(присваивания), не состоящие в списке, разделенном запятыми, то такие привязки будут изменять значение существующей внешней переменной - это изменение будет видно за пределами блока. Если переменной с таким именем не существовало, за пределами блока она исчезнет.

rv = 0
m = 123 # оригинальная m
s1 = "code 345"

let m = match(r"(\d+)", s1) # let-привязка m
    # старая m недоступна отсюда
    rv = m!=nothing ? parse(Int,m[1]) : 0
end # блок закончен, все let-привязки забыты

println(m) 
# 123 # - старое значение
println(rv) 
# 345 # rv - изменилась 
# т.к. ее привязка не была в let-списке

Т.к. let...end возвращает последнее вычисленное выражение, то такой более лаконичный код будет работать:

m = 123
s1 = "code 345"

rv = let m = match(r"(\d+)", s1)
    m!=nothing ? parse(Int,m[1]) : 0
end

Здесь мы не инициализируем заранее rv нулем.

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

Использование диспетчеризации

Этот вариант кода основан на том, что если регулярное выражение не подошло, то функция match возвращает nothing (типа Void), а если подошло - объект типа RegexMatch.

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

myarray = ["code 234", "not a code", "code 456"]

f(m::RegexMatch) = parse(Int,m[1]) # -функция f для аргумента типа RegexMatch

f(::Void) = 0 # -функция f для аргумента, равного nothing

map( _->match(r"(\d+)", _)|>f, myarray) # -анонимная функция _->match(r"(\d+)", _) передаст результат в f

# возвращенное значение:
3-element Array{Int64,1}:
234
  0
456

Может быть, такая форма записи кажется слишком "по-хакерски" запутанной? Поработаем над этим.
Вызов map может выглядеть так (без оператора |> и с именем параметра el вместо _):

map( el->f( match( r"(\d+)", el), myarray)

или так (с использованием do-синтаксиса, который из блока кода do...end создает анонимную функцию и передает ее первым параметром в вызванную функцию):

map( myarray) do el
  f( match( r"(\d+)", el))
end

или так:

map( myarray) do el
  m = match( r"(\d+)", el)
  f(m)
end

В отличие от let , эта привязка m внутри тела функции будет однозначно локальной. Она гарантировано не затронет внешних переменных.

Это называется жесткой областью видимости. Присваивание переменной создает новую локальную переменную, если только она не помечена внутри функции как global. Переменные для чтения - наследуются из родительской области видимости, если только не помечены как local.

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

results matching ""

    No results matching ""