Как стать автором
Обновить

Комментарии 39

Судя по первому абзацу думал что будет парсинг питона с поддержкой
if foo = make_foo():
  # do work with foo

Аналогично, логика повествования явно где-то хромает

А чем лучше на C++ во втором варианте? В будущих проблемах с утечками памяти? Такой вариант разве что с умными указателями имеет смысл, но никак не с обычными.

На мой взгляд, даже хуже. Хуже для глаз в первую очередь.
Все же уже есть какие то сложившиеся шаблоны в восприятии исходников.
Шаблоны, в которых сознательная часть мозга не используется. А новый синтаксис просто срывает это шаблон.
А в чем преимущество этого варианта написания — лично я не вижу. Экономим перевод строки?


Так и не понял, почему автору "if" не нравится. Вообще… как концепция.

Ну в данном случае прям как в for получается, только без блока инкрементации. Но и согласен с вами, вообще непонятно зачем добавили эту бяку, когда есть действительно полезные вещи, которые не пускают в стандарт.

Ну единственное применение для такой конструкции "if", что я вижу — ограничение области видимости переменной (жизни ее на стеке… например).
Но для демонстрации этого, пример автором выбран не удачный с указателем.


Хотя, лично мне, как то не лень пару "{}" поставить. Более общее и наглядное и, главное, совместимое по исходникам решение.


А насчет "for"… До сих пор под некоторое железо (старые модели Verifone PINPAD) приходится пользовать кросс компиляторы где, "i" в выражении "for(int i=..." не является ограниченным в видимости "for".


Увы… все эти новые фичи языка интересны, но приходится писать так, что бы код был переносим всегда.

Удачи в поддержке всего этого. Особенно когда автор уже уволился, а комментариев или нет, или кот наплакал. Всё это выглядит очень красиво и изящно, пока пишешь сам. Но если это не проектик just for fun, приготовься к тому, что какой-то бедолага будет матюкать тебя самым площадным-беспощадным образом, пытаясь выяснить, что за траву ты курил статьи на Хабре ты читал, когда писал это.
Если я напишу генератор этого по БНФ, бедолага станет лучше себя чувствовать?

Понял бы вас, если бы речь шла о паре точечных фокусов, но если это парадигма целого модуля, возмущение бедолаги звучит как «они использовали функциональное программирование, а мне теперь в нём разбираться, вот дураки».

Готов поспорить, этот аргумент слышали разработчики Питона, Джанго, Реакта… список можно продолжать.

Справедливости ради, именно приведенный пример можно написать и на С++03:

if ((Foo* foo = make_foo()))
{
}
И с одной скобкой можно же? Ну и не только на С++03, оператор присваивания всегда возвращал lvalue.
А, точно.
Это странный анализатор, если он не отличает однозначное объявление переменной в
if (Foo* foo = make_foo())

и возможность перепутать сравнение с присваиванием в
if (foo = make_foo())

Ничего странного. Первое, вообще‐то, может и умножением быть. Анализатору, конечно, такое очевидно, но он не должен предполагать, что это также очевидно пользователю.


Хотя я бы ещё предположил, что к тому времени, как такое присваивание дойдёт до анализатора, информации о том, что там есть и объявление переменной у него уже не будет.

Не пробовали для разбора текста на лексемы использовать *flex*?
Я попробовал обойтись без зависимостей.
А, по-моему, с готовым качественным DSL выглядит намного изящнее, чем ваш велосипед (да и пишется быстрее).
Скажем, github.com/vlasovskikh/funcparserlib:
def parse(seq):
    'Sequence(Token) -> object'
    ...
    n = lambda s: a(Token('Name', s)) >> tokval
    def make_array(n):
        if n is None:
            return []
        else:
            return [n[0]] + n[1]
    ...
    null = n('null') >> const(None)
    true = n('true') >> const(True)
    false = n('false') >> const(False)
    number = toktype('Number') >> make_number
    string = toktype('String') >> make_string
    value = forward_decl()
    member = string + op_(':') + value >> tuple
    object = (
        op_('{') +
        maybe(member + many(op_(',') + member)) +
        op_('}')
        >> make_object)
    array = (
        op_('[') +
        maybe(value + many(op_(',') + value)) +
        op_(']')
        >> make_array)
    value.define(
          null
        | true
        | false
        | object
        | array
        | number
        | string)
    json_text = object | array
    json_file = json_text + skip(finished)

    return json_file.parse(seq)
Таки у вас либа. С либами я и не соревнуюсь.

Не сказал бы, что полный код примера funcparserlib радикально выигрывает в изящности или "понятности с первого взгляда". И по числу строчек примерно соответствует (144 против 130). Так что не стоит спешить обвинять автора в велосипедостроении. То, что он нам представил, — это в первую очередь упражнение, которое небесполезно было бы повторить самому. Это заслуживает и плюса, и благодарности за проделанную работу.

Имхо, выигрывает. В funcparserlib нет лишних управляющих структур (for/yield), которые есть в коде из статьи. Это значит, что без этих структур можно обойтись, и следовательно они не нужны.
И что-то мне подсказывает, что по 2 yield'а в каждой функции — не Python-way (это не выглядит просто).

1. В C++ давным давно можно делать так:

if (Foo* foo = make_foo())
{
// do something with foo, it is != nullptr
}
// no foo here


2. Рекурсия в parse_comma_separated_values — это провал. «Извините, ваш список слишком длинный, у меня стэк кончился».

3. Ну и сколько гбайт/с пережевывает этот парсер? Ну хотя бы сотен мегабайт/с?

4. Где изящество-то?
1. Пример поправил.
2. Да, действительно, на очень больших списках будет так себе. Но можно переписать без рекурсии:
def parse_comma_separated_values(src):
    result = []
    for value, src in parse_value(src): # достаем первый элемент (mommy says I'm special)
        result.append(value)
        break
    else:
        return #  ни одного элемента не получается достать, ну упс
    while src:
        for (_, value), src in sequence(parse_comma, parse_value):
            result.append(value)
            break
        else:
            break # все сломалось, возвращаем сколько нашли
    yield result, src

А это уже можно завернуть в ещё одну отдельную функцию, чтобы было вообще parse_list(item=parse_value, delimiter=parse_comma). Но в статье я решил сделать упор в сторону лаконичности.
3. Воу-воу, если бы мне нужно было пережёвывать гигабайты в секунду, я бы спокойно и академично написал на С с lex-yacc.
4. В заголовке.
3. Ну и тогда возникает вопрос (без троллинга): зачем вообще учиться писать recursive descent parser'ы? Это примерно как bubble sort — сортирует, конечно, но для решения практических задач так не надо делать НИКОГДА. Ещё и на питоне.
Тут вы не правы. Практическая задача™ не всегда требует парсинга гигабайт в секунду. Иногда, например, бывает нужно в Python-проекте распарсить консольный вывод какой-нибудь медленной программы. Парсер нужен, желательно на Питоне (чтобы не усложнять сборку), скорость не критична, расширяемость желательна. Вот и пример.

Комбинаторные парсеры (пример выше) выглядят гораздо "изящнее", чем for'ы вместо последовательностей термов. (Если эти for'ы убрать, то и получатся комбинаторные парсеры. А так — "код в стиле барокко").
Рекурсивный спуск выглядит проще (но объёмнее).
И ещё есть всякие генераторы парсеров, которые должны работать быстрее.


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


И почему Python 2 ?

И почему про парсинг?
И почему на Хабре?
И почему на русском языке?

(Я не знаю ответа на ваш вопрос, но чувствую, что он как-то связан с ответами на приведённые вопросы...)
«for», на сколько я понял, здесь используется как «do» в Haskell. То есть вполне в стиле монадических парсеров.

Да, но do-синтаксис менее многословен и семантически больше подходит.

Я когда-то тоже довольно долго мучился с if'ами при необходимости сматчить строку с одной из нескольких регулярок. Но потом мне всё-таки удалось сконцентрировать волю и сделать тривиальную обёртку вокруг re.

Обёртка
class Re(object):
    def __init__(self):
        self.last_match = None

    def match(self, pattern, text):
        self.last_match = re.match(pattern, text)
        return self.last_match

    def search(self, pattern, text):
        self.last_match = re.search(pattern, text)
        return self.last_match

    def group(self, index):
        return self.last_match.group(index)


Теперь я делаю вот так и забыл о прежнем чувстве дискомфорта:

Приятные if'ы
r = Re()
if r.search(r'[0-9]+', str):
    # число
elif r.search(r'[a-z]+', str):
    # слово


Понимаю, что это не совсем по теме, поскольку не про парсер. Но просто вспомнилось, когда увидел свои прежние if'ы…
В BNF нужно заменить "," на "','".
Действительно, спасибо!
на Python 2


Меня как из ушата окатили. Так гадко стало. И обидно.
Я не очень читаю питон, но по моему Вы переизобрели комбинаторные парсеры. Если в них поддержать монадические операции, в сложных случаях код можно упростить (хотя для json это только в парсере строк вместо регекспов поможет).
Вот явно же здесь монада Maybe проглядывает!
А генераторы — это способ по-быстрому оформить ленивость.

Ну и начинать надо было с обёртки над match
def maybe(x):
  ''' lift optional value (maybe None) to the single-item-list monad '''
  return [v for v in [x] if v is not None]

def maybe_match(expr, g1, g2, src):
  ''' expr - regular expression, g1 - identifier of token group, g2 - identifier of the rest, src - source '''
  return [m.group(g1,g2) for m in maybe(re.match(expr, src)]

. . .
for token,rest in maybe_match(r'([+\-]?[0-9]+)(.*)', 1,2, src):
   . . .

Вопрос, без подковырок: а зачем избавляться от if, если с ними даже написаный на коленке на скорую руку парсер получается короче минимум раза в 2? Не модно? В чём практический смысл?

например
def tkn(iv):
	fv = {'null':None, 'true':True, 'false':False}
	s = iv.strip()
	if s in fv.keys():
		rv = fv[s]
	else:
		try:
			rv = float(s) if ('.' in s) | ('e' in s) | ('E' in s) else int(s)
		except ValueError:
			rv = s[1:-1] if (s[0] == s[-1]) and s[0] in "\"'" else s
	return rv

def arr(iv):
	rv = []
	tkn = ""
	ignore = False
	if iv[0] == '[' and iv[-1] == ']':
		for c in iv[1:-1]:
			if c in "'\"[]{}":
				ignore = not ignore
			tkn += '' if c == ',' and not ignore else c
			if (c == ',') and not ignore:
				rv.append(parse(tkn))
				tkn = ""
		rv.append(parse(tkn))
	return rv

def split(iv):
	s = iv.strip()
	i = s[1:].find(s[0])
	j = s[i+1:].find(':')
	return [s[1:i+1], s[i+j+2:]] if i > -1 and j > -1 else []

def dict(iv):
	rv = {}
	tkn = ""
	p = []
	ignore = False
	if iv[0] == '{' and iv[-1] == '}':
		for c in iv[1:-1]:
			if c in "'\"{}[]":
				ignore = not ignore
			tkn += '' if c == ',' and not ignore else c
			if (c == ',') and not ignore:
				p = split(tkn)
				if len(p) > 0:
					rv[p[0]] = parse(p[1])
				tkn = ""
		p = split(tkn)
		if len(p) > 0:
			rv[p[0]] = parse(p[1])
	return rv

def parse(iv):
	s = iv.strip()
	return None if len(s) < 1 else arr(s) if s[0] == '[' else ( dict(s) if s[0] == '{' else tkn(s) )

Вам самому-то ваш код как, нравится? Представьте, что по нему нужно понять, что он парсит. А теперь представьте, что где-то что-то потребовалось поменять.
Вы не ответили на вопрос. В чём практический смысл избавления от if?

Нравится код или нет в данном случае совсем не причём. Это код, написаный на скорую руку, для примера. И, кстати, понять что он парсит и поменять по желанию, как минимум, ничуть не сложнее вашего.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории