Определение совпадающих скобок в Haskell

Предполагая, что у меня есть строка, например:

abc(def(gh)il(mn(01))afg)lmno(sdfg*)

Как я могу определить соответствующую скобку для первого? (имеется в виду (def(gh)il(mn(01))afg))

Я попытался создать функцию между, подсчитав количество открытых скобок до первого ')', но это не работает с такими строками.


person Iulia Muntianu    schedule 20.04.2012    source источник
comment
Нет, я пытаюсь преобразовать строку в созданный мной тип данных, и это единственная функция, которую я не могу заставить работать.   -  person Iulia Muntianu    schedule 20.04.2012
comment
Вам следует изучить Parsec, который превосходно справляется с задачами такого рода. Не зная больше о том, какую структуру данных вы хотите разобрать, мне трудно дать вам пример кода.   -  person Chris Taylor    schedule 20.04.2012


Ответы (2)


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

Например:

parenPairs :: String -> [(Int, Int)]
parenPairs = go 0 []
  where
    go _ _        []         = []
    go j acc      ('(' : cs) =          go (j + 1) (j : acc) cs
    go j []       (')' : cs) =          go (j + 1) []        cs -- unbalanced parentheses!
    go j (i : is) (')' : cs) = (i, j) : go (j + 1) is        cs
    go j acc      (c   : cs) =          go (j + 1) acc       cs

Эта функция возвращает список всех пар индексов, принадлежащих парам совпадающих скобок.

Применение функции к вашей строке примера дает:

> parenPairs "abc(def(gh)il(mn(01))afg)lmno(sdfg*)"
[(7,10),(16,19),(13,20),(3,24),(29,35)]

Интересующая вас открывающая скобка отображается в индексе 3. Возвращаемый список показывает, что соответствующая закрывающая скобка находится в индексе 24.

Следующие функции дают вам все правильно заключенные в скобки сегменты строки:

parenSegs :: String -> [String]
parenSegs s = map (f s) (parenPairs s)
  where
    f s (i, j) = take (j - i + 1) (drop i s)

Например:

> parenSegs "abc(def(gh)il(mn(01))afg)lmno(sdfg*)"
["(gh)","(01)","(mn(01))","(def(gh)il(mn(01))afg)","(sdfg*)"]

Следуя предложению Фрериха Раабе, теперь мы также можем написать функцию, которая возвращает только крайний левый сегмент:

firstParenSeg :: String -> String
firstParenSeg s = f s (minimum (parenPairs s))
  where
    f s (i, j) = take (j - i + 1) (drop i s)

Например:

> firstParenSeg "abc(def(gh)il(mn(01))afg)lmno(sdfg*)"
"(def(gh)il(mn(01))afg)"

Обратите внимание, что firstParenSeg завершится ошибкой, если входная строка не содержит хотя бы одной пары совпадающих скобок.

Наконец, небольшая адаптация функции parenPairs позволяет ей не работать с несбалансированными скобками:

parenPairs' :: String -> [(Int, Int)]
parenPairs' = go 0 []
  where
    go _ []        []         = []
    go _ (_ : _ )  []         = error "unbalanced parentheses!"
    go j acc       ('(' : cs) =          go (j + 1) (j : acc) cs
    go j []        (')' : cs) = error "unbalanced parentheses!"
    go j (i : is)  (')' : cs) = (i, j) : go (j + 1) is        cs
    go j acc       (c   : cs) =          go (j + 1) acc       cs
person Stefan Holdermans    schedule 20.04.2012
comment
Я думаю, что функция parenSegs почти отвечает на исходный вопрос: How can I determine the matching bracket for the first one?. Для этого вы можете сопоставить f s только с minimumBy (comparing fst) . parenPairs (что даст вам начальную/конечную позицию группы для первой открывающей скобки в строке). - person Frerich Raabe; 20.04.2012
comment
Верно. Придирка: с тем же успехом вы могли бы написать minimum . parenParens: индуцированный Ord порядок целочисленных пар отлично подходит для этой цели. - person Stefan Holdermans; 20.04.2012
comment
Аккуратный! Я не знал, что целочисленные пары упорядочиваются по умолчанию - спасибо, что указали на это. :-) - person Frerich Raabe; 20.04.2012
comment
Теперь я включил ваше предложение в свой ответ. - person Stefan Holdermans; 20.04.2012
comment
Слишком полный ответ для неполного вопроса. По крайней мере, оставьте часть кода ненаписанным, чтобы ОП сам разобрался. Это хороший ответ, поэтому это отменяет мое предложение. +0 - person luqui; 20.04.2012

Простое решение для новичков с использованием вспомогательной функции go.

brackets :: String -> String
brackets string = go string 0 False
  where go (s:ss) 0 False | s /= '(' = go ss 0 False
        go ('(':ss) 0 False = '(' : go ss 1 True
        go (')':_) 1 True = ")"
        go (')':ss) n True = ')' : go ss (n-1) True
        go ('(':ss) n True = '(' : go ss (n+1) True
        go (s:ss) n flag = s : go ss n flag 
        go "" _ _ = ""

Идея состоит в том, чтобы запомнить некоторый счетчик открытия скобок для каждого Char. И когда этот счетчик будет равен 1, а Char равен ) - пора возвращать нужную строку.

> brackets "abc(def(gh)il(mn(01))afg)lmno(sdfg*)"
"(def(gh)il(mn(01))afg)"

Обратите внимание, что эта функция вернет строку с незакрытой скобкой для несбалансированной строки, например:

> brackets "a(a(a"
"(a(a"

Этого можно было бы избежать с помощью другого условия сопоставления с образцом.


UPD:

Более читаемым решением является balancedSubstring функция :: String -> Maybe String, которая возвращает Just требует подстроки, если скобки сбалансированы, и Nothing в других случаях.

brackets :: String -> String
brackets string = go string 0 False
  where go (s:ss) 0 False | s /= '(' = go ss 0 False
        go ('(':ss) 0 False = '(' : go ss 1 True
        go (')':_) 1 True = ")"
        go (')':ss) n True = ')' : go ss (n-1) True
        go ('(':ss) n True = '(' : go ss (n+1) True
        go (s:ss) n flag = s : go ss n flag
        go "" _  _ = ""

isBalanced :: String -> Bool
isBalanced string = go string 0
  where go ('(':ss) n = go ss (n+1)
        go (')':ss) n | n > 0 = go ss (n-1)
        go (')':_ ) n | n < 1 = False
        go (_:ss) n = go ss n
        go "" 0 = True
        go "" _ = False

balancedSubstring :: String -> Maybe String
balancedSubstring string | isBalanced string = Just $ brackets string
balancedSubstring string | otherwise         = Nothing

Итак, теперь результат функции balancedSubstring более понятен:

> balancedSubstring "abc(def(gh)il(mn(01))afg)lmno(sdfg*)"
Just "(def(gh)il(mn(01))afg)"

> balancedSubstring "a(a(a"
Nothing
person ДМИТРИЙ МАЛИКОВ    schedule 20.04.2012