Как взломать GHCi (или Hugs), чтобы он печатал символы Unicode без экранирования?

Посмотрите на проблему: обычно в интерактивной среде Haskell нелатинские символы Unicode (которые составляют часть результатов) печатаются с экранированием, даже если локаль разрешает такие символы (в отличие от прямого вывода через putStrLn, putChar, который выглядит хорошо и читабельно) — примеры показывают GHCi и Hugs98:

$ ghci
GHCi, version 7.0.1: http://www.haskell.org/ghc/  :? for help
Prelude> "hello: привет"
"hello: \1087\1088\1080\1074\1077\1090"
Prelude> 'Я'
'\1071'
Prelude> putStrLn "hello: привет"
hello: привет
Prelude> :q
Leaving GHCi.
$ hugs -98
__   __ __  __  ____   ___      _________________________________________
||   || ||  || ||  || ||__      Hugs 98: Based on the Haskell 98 standard
||___|| ||__|| ||__||  __||     Copyright (c) 1994-2005
||---||         ___||           World Wide Web: http://haskell.org/hugs
||   ||                         Bugs: http://hackage.haskell.org/trac/hugs
||   || Version: September 2006 _________________________________________

Hugs mode: Restart with command line option +98 for Haskell 98 mode

Type :? for help
Hugs> "hello: привет"
"hello: \1087\1088\1080\1074\1077\1090"
Hugs> 'Я'
'\1071'
Hugs> putStrLn "hello: привет"
hello: привет

Hugs> :q
[Leaving Hugs]
$ locale
LANG=ru_RU.UTF-8
LC_CTYPE="ru_RU.UTF-8"
LC_NUMERIC="ru_RU.UTF-8"
LC_TIME="ru_RU.UTF-8"
LC_COLLATE="ru_RU.UTF-8"
LC_MONETARY="ru_RU.UTF-8"
LC_MESSAGES="ru_RU.UTF-8"
LC_PAPER="ru_RU.UTF-8"
LC_NAME="ru_RU.UTF-8"
LC_ADDRESS="ru_RU.UTF-8"
LC_TELEPHONE="ru_RU.UTF-8"
LC_MEASUREMENT="ru_RU.UTF-8"
LC_IDENTIFICATION="ru_RU.UTF-8"
LC_ALL=
$ 

Мы можем предположить, что это потому, что print и show используются для форматирования результата, и эти функции делают все возможное, чтобы отформатировать данные каноническим, максимально переносимым способом — поэтому они предпочитают экранировать странные символы (возможно, это даже прописано в стандарте для Haskell):

$ ghci
GHCi, version 7.0.1: http://www.haskell.org/ghc/  :? for help
Prelude> show 'Я'
"'\\1071'"
Prelude> :q
Leaving GHCi.
$ hugs -98
Type :? for help
Hugs> show 'Я'
"'\\1071'"
Hugs> :q
[Leaving Hugs]
$ 

Но все же было бы неплохо, если бы мы знали, как взломать GHCi или Hugs, чтобы напечатать эти символы в удобном для человека виде, то есть напрямую, без экранирования. Это можно оценить при использовании интерактивной среды Haskell в образовательных целях, для обучения/демонстрации Haskell перед неанглоязычной аудиторией, которой вы хотите показать некоторые данные Haskell на их человеческом языке.

На самом деле, это полезно не только для образовательных целей, но и для отладки! Когда у вас есть функции, определенные для строк, представляющих слова других языков, с символами, отличными от ASCII. Итак, если программа зависит от языка, и в качестве данных имеют смысл только слова другого языка, и у вас есть функции, которые определены только для таких слов, для отладки в GHCi важно видеть эти данные.

Подводя итог моему вопросу: какие есть способы взломать существующие интерактивные среды Haskell для более удобной печати Unicode в результатах? («Дружелюбие» в моем случае означает даже «проще»: я бы хотел, чтобы print в GHCi или Hugs показывало нелатинские символы простым прямым способом, как это делается putChar, putStrLn, то есть без экранирования.)

(Возможно, помимо GHCi и Hugs98, я также посмотрю на существующие режимы Emacs для взаимодействия с Haskell, чтобы увидеть, могут ли они представить результаты в красивой, неэкранированной форме.)


person imz -- Ivan Zakharyaschev    schedule 04.04.2011    source источник
comment
Вероятно, вы имеете в виду не-(для печати ASCII) вместо не-латиницы.   -  person tc.    schedule 12.04.2011
comment
@tc, почему твой комментарий ценен? Я просто не понимаю, как эта смена терминологии может помочь. Возможно, это также может ввести в заблуждение, потому что я привык думать, что нелатинские символы, которые мне здесь интересны (кириллица), печатаются напрямую (в соответствующих локалях, как у меня). Как видно из тестов, в данном случае нелатинские символы являются подмножеством не-(печатаемых ASCII), потому что я пытаюсь получить результат с напечатанными такими символами, а они экранируются. Меня не интересуют другие непечатаемые символы, кроме букв (которые, как я предполагаю, печатаются непосредственно в моей локали).   -  person imz -- Ivan Zakharyaschev    schedule 12.04.2011
comment
Вижу, ты меня переплюнул.   -  person tc.    schedule 13.04.2011
comment
@imz: Даже латинские символы, отличные от ASCII, не печатаются: ä ->> \228 в GHCi и Hugs...   -  person false    schedule 28.10.2012
comment
@false, я вижу, ты продолжаешь строку этого исправляющего комментария; поэтому было бы точнее сказать в моем вопросе что-то вроде букв, отличных от ASCII (= неанглийских букв). Основное внимание в этом исправлении должно было быть уделено включению ASCII, а не возможности печати в каком-то смысле. Тогда я согласен, что это правильные слова для этой проблемы.   -  person imz -- Ivan Zakharyaschev    schedule 29.10.2012
comment
@imz: ASCII даже не распространяется на английский язык, подумайте о наивности, роли, упреждении, œuvre. Это просто ASCII.   -  person false    schedule 29.10.2012
comment
@false (Спасибо за ссылку на грамматику определенных предложений! Я думаю, что это было бы более полезно в вашем вопросе в контексте DCG на CSTheory, чтобы мы могли увидеть больше информации о том, что вы задаете такой вопрос. -- cstheory.stackexchange.com/questions/14006/ )   -  person imz -- Ivan Zakharyaschev    schedule 29.10.2012
comment
@false Ну, я имел в виду: буквы английского алфавита. Список букв, которые есть в словарях английского языка. Список букв, приведенный в статье в Википедии. Ну вы поняли, о каком списке букв я говорю.   -  person imz -- Ivan Zakharyaschev    schedule 29.10.2012


Ответы (7)


Вариант 1 (плохой):

Измените эту строку кода:

https://github.com/ghc/packages-base/blob/ba98712/GHC/Show.lhs#L356

showLitChar c s | c > '\DEL' =  showChar '\\' (protectEsc isDec (shows (ord c)) s)

И перекомпилировать ghc.

Вариант 2 (много работы):

Когда тип GHCi проверяет проанализированный оператор, он попадает в tcRnStmt, который опирается на mkPlan (оба в https://github.com/ghc/ghc/blob/master/compiler/typecheck/TcRnDriver.lhs). Это пытается проверить несколько вариантов оператора, который был введен, включая:

let it = expr in print it >> return [coerce HVal it]

Конкретно:

print_it  = L loc $ ExprStmt (nlHsApp (nlHsVar printName) (nlHsVar fresh_it))
                                      (HsVar thenIOName) placeHolderType

Все, что здесь может потребоваться изменить, это printName (которое привязывается к System.IO.print). Если вместо этого он привязан к чему-то вроде printGhci, который был реализован как:

class ShowGhci a where
    showGhci :: a -> String
    ...

-- Bunch of instances?

instance ShowGhci Char where
    ...  -- The instance we want to be different.

printGhci :: ShowGhci a => a -> IO ()
printGhci = putStrLn . showGhci

Затем Ghci может изменить то, что напечатано, включив в контекст разные экземпляры.

person fryguybob    schedule 11.04.2011
comment
Однако изменение семантики show не очень Хаскелли. Лучше написать новый класс шоу или определить новую команду в ghci для такого рода показа. - person Don Stewart; 12.04.2011
comment
Большое спасибо за копание в коде! Так что теперь патч должен от нас (от меня). - person imz -- Ivan Zakharyaschev; 22.04.2011
comment
@Don Может ли переход на другую версию show с опцией GHC считаться Хаскелли? Или, скорее, нет, потому что опции не должны менять семантику функций, а только возможности компилятора? Но есть ли законные способы перейти на альтернативные реализации стандартных функций? (Такое отображение (разыскиваемый вариант) на самом деле не является большим отклонением от предполагаемой семантики show, просто делает его более удобным для человека.) - person imz -- Ivan Zakharyaschev; 22.04.2011
comment
@Don Чтобы сохранить работу по определению экземпляров для альтернативного класса Show (например, ShowGhci), может возникнуть соблазн использовать существующие экземпляры Show по умолчанию, только переопределив экземпляр для String и Char. Но это не сработает, потому что если вы используете showGhci = show, то для любых сложных данных, содержащих строки, show сложно скомпилировать, чтобы вызвать старый show для отображения строки. В этой ситуации требуется возможность передавать разные словари, реализующие один и тот же интерфейс класса, в функции, которые используют этот интерфейс (show передаст его subshows). Какие-нибудь расширения GHC для этого? - person imz -- Ivan Zakharyaschev; 08.03.2015
comment
Возможно, можно было бы скрыть экземпляры, которые мы хотим изменить при импорте, и определить наши собственные экземпляры. Будут ли тогда наши собственные экземпляры использоваться во всей программе (или модуле)?.. - person imz -- Ivan Zakharyaschev; 19.06.2015

Один из способов взломать это — обернуть GHCi в оболочку оболочки, которая читает его стандартный вывод и неэкранирует символы Unicode. Конечно, это не путь Haskell, но он работает :)

Например, это оболочка ghci-esc, которая использует sh и python3 (здесь важно 3):

#!/bin/sh

ghci "$@" | python3 -c '
import sys
import re

def tr(match):
    s = match.group(1)
    try:
        return chr(int(s))
    except ValueError:
        return s

for line in sys.stdin:
    sys.stdout.write(re.sub(r"\\([0-9]{4})", tr, line))
'

Использование ghci-esc:

$ ./ghci-esc
GHCi, version 7.0.2: http://www.haskell.org/ghc/  :? for help
> "hello"
"hello"
> "привет"
"привет"
> 'Я'
'Я'
> show 'Я'
"'\Я'"
> :q
Leaving GHCi.

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

person Andrey Vlasovskikh    schedule 10.04.2011
comment
Спасибо за код и за идею! Я, вероятно, просто попытаюсь написать аналогичную оболочку на Haskell, так как на данный момент у меня не установлен python3. - person imz -- Ivan Zakharyaschev; 22.04.2011
comment
Я заменяю r\([0-9]{4}) на r\([0-9]+), и это должно работать, если оно содержит более 4 цифр. - person TorosFanny; 18.01.2015
comment
Но у него есть проблема — после того, как вы нажмете «Ctrl-c», он перестанет печатать вообще что-либо, кроме ошибки о сломанной трубе, пока вы не перезагрузите ghci. - person Hi-Angel; 29.03.2015
comment
@ Hi-Angel Вы можете обернуть for line in sys.stdin в try: ... except KeyboardInterrupt: pass - person Andrey Vlasovskikh; 29.03.2015
comment
Вы можете найти другие оболочки вокруг GHCi по адресу wiki.haskell.org/GHCi_in_colour, чтобы их можно было повторно использовать. используется не для раскрашивания, а для перекодирования. (Теоретически.) - person imz -- Ivan Zakharyaschev; 08.04.2015

В этом вопросе достигнут некоторый прогресс; спасибо bravit (Виталий Брагилевский)!:

Вероятно, включено в GHC 7.6.1. (Это?..)

Как заставить теперь печатать кириллицей:

Параметр, передаваемый GHCi, должен быть функцией, которая может печатать кириллицей. На Hackage такой функции не обнаружено. Итак, нам нужно создать простую обертку, как сейчас:

module UPPrinter where
import System.IO
import Text.PrettyPrint.Leijen

upprint a = (hPutDoc stdout . pretty) a >> putStrLn ""

И запустите ghci таким образом: ghci -interactive-print=UPPrinter.upprint UPPrinter

Конечно, это можно раз и навсегда записать в .ghci.

Практическая задача: придумать альтернативный красивый Show

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

Использование чужой работы: другие симпатичные принтеры

Выше предлагается Text.PrettyPrint.Leijen, вероятно, потому, что известно, что такие символы не экранируются в строках.

Наше собственное Шоу, основанное на Шоу — привлекательное, но непрактичное.

Как насчет написания собственных Show, скажем, ShowGhci, как было предложено в ответе здесь. Это практично?..

Чтобы сохранить работу по определению экземпляров для альтернативного класса Show (например, ShowGhci), может возникнуть соблазн использовать существующие экземпляры Show по умолчанию, только переопределив экземпляры для String и Char. Но это не сработает, потому что если вы используете showGhci = show, то для любых сложных данных, содержащих строки show, будет "жестко скомпилирован" вызов старого show для отображения строки. В этой ситуации требуется возможность передавать разные словари, реализующие один и тот же интерфейс класса, функциям, использующим этот интерфейс (show передаст его subshows). Любые расширения GHC для этого?

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

Повторный разбор show

Более практичное (и короткое) решение находится в другом ответе здесь: проанализируйте вывод из show для обнаружения символов и строк и переформатируйте их. (Хотя семантически это кажется немного уродливым, решение короткое и безопасное в большинстве случаев (если в show нет кавычек, используемых для других целей; не должно быть в случае со стандартными вещами, потому что идея show состоит в том, чтобы быть больше или больше). -менее корректный анализируемый Haskell.)

Семантические типы в ваших программах

И еще одно замечание.

На самом деле, если нас интересует отладка в GHCi (а не просто демонстрация Haskell и желание получить красивый вывод), необходимость отображения букв, отличных от ASCII, должна исходить из неотъемлемого присутствия этих символов в вашей программе (иначе для отладки, вы можете заменить их латинскими символами или не заботиться о том, чтобы отображались коды). Другими словами, в этих символах или строках есть некий СМЫСЛ с точки зрения предметной области. (Например, я недавно занимался грамматическим анализом русского языка, и русские слова в составе словаря-примера присутствовали в моей программе «по своей сути». Ее работа имела бы смысл только с этими конкретными словами. Поэтому мне нужно было читайте их при отладке.)

Но смотрите, если струны имеют какое-то ЗНАЧЕНИЕ, тогда они уже не простые струны; это данные осмысленного типа. Возможно, программа стала бы еще лучше и безопаснее, если бы вы объявили специальный тип для такого рода значений.

И затем, ура!, вы просто определяете свой экземпляр Show для этого типа. И у вас все в порядке с отладкой вашей программы в GHCi.

Например, в моей программе для грамматического анализа я сделал:

newtype Vocable = Vocable2 { ortho :: String } deriving (Eq,Ord)
instance IsString Vocable -- to simplify typing the values (with OverloadedStrings)
    where fromString = Vocable2 . fromString

а также

newtype Lexeme = Lexeme2 { lemma :: String } deriving (Eq,Ord)
instance IsString Lexeme -- to simplify typing the values (with OverloadedStrings)
    where fromString = Lexeme2 . fromString

(дополнительное fromString здесь потому, что я могу переключить внутреннее представление с String на ByteString или что-то еще)

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

person imz -- Ivan Zakharyaschev    schedule 28.10.2012
comment
Это здорово и приятно :), что этот запрос на функцию привел к функции, которую оценило довольно много программистов. Примеры его использования: логирование из GHCi, раскрашивание вывода GHCi. - person imz -- Ivan Zakharyaschev; 08.04.2015

Ситуация изменится в следующей версии 7.6.1 Ghci, поскольку она предоставляет новую опцию Ghci под названием: -interactive-print. Вот скопировано из ghc-manual: (А myShow и myPrint я написал так)

2.4.8. Using a custom interactive printing function

[New in version 7.6.1] By default, GHCi prints the result of expressions typed at the prompt using the function System.IO.print. Its type signature is Show a => a -> IO (), and it works by converting the value to String using show.

This is not ideal in certain cases, like when the output is long, or contains strings with non-ascii characters.

The -interactive-print flag allows to specify any function of type C a => a -> IO (), for some constraint C, as the function for printing evaluated expressions. The function can reside in any loaded module or any registered package.

As an example, suppose we have following special printing module:

     module SpecPrinter where
     import System.IO

     sprint a = putStrLn $ show a ++ "!"

The sprint function adds an exclamation mark at the end of any printed value. Running GHCi with the command:

     ghci -interactive-print=SpecPrinter.sprinter SpecPrinter

will start an interactive session where values with be printed using sprint:

     *SpecPrinter> [1,2,3]
     [1,2,3]!
     *SpecPrinter> 42
     42!

A custom pretty printing function can be used, for example, to format tree-like and nested structures in a more readable way.

The -interactive-print flag can also be used when running GHC in -e mode:

     % ghc -e "[1,2,3]" -interactive-print=SpecPrinter.sprint SpecPrinter
     [1,2,3]!


module MyPrint (myPrint, myShow) where
-- preparing for the 7.6.1
myPrint :: Show a => a -> IO ()
myPrint = putStrLn . myShow

myShow :: Show a => a -> String
myShow x = con (show x) where
  con :: String -> String
  con [] = []
  con li@(x:xs) | x == '\"' = '\"':str++"\""++(con rest)
                | x == '\'' = '\'':char:'\'':(con rest')
                | otherwise = x:con xs where
                  (str,rest):_ = reads li
                  (char,rest'):_ = reads li

И они хорошо работают:

*MyPrint> myPrint "asf萨芬速读法"
"asf萨芬速读法"
*MyPrint> myPrint "asdffasdfd"
"asdffasdfd"
*MyPrint> myPrint "asdffa撒旦发"
"asdffa撒旦发"
*MyPrint> myPrint '此'
'此'
*MyPrint> myShow '此'
"'\27492'"
*MyPrint> myPrint '此'
'此'
person TorosFanny    schedule 22.01.2013
comment
Спасибо за Ваш ответ! Да, это отличная маленькая функция! Я следил за этим и уже обобщил эту новую функцию 7.6.1 здесь. - person imz -- Ivan Zakharyaschev; 17.04.2013
comment
Повторная обработка работает! Чтобы сэкономить работу по определению экземпляров для нового класса Show (например, ShowGhci), может возникнуть соблазн использовать существующие экземпляры Show по умолчанию, только переопределив экземпляры для String и Char. Но это не сработает, потому что если вы используете myShow = show, то для любых сложных данных, содержащих строки, show сложно скомпилировать, чтобы вызвать старый show для отображения строки. В этой ситуации требуется возможность передавать разные словари, реализующие один и тот же интерфейс класса, в функции, которые используют этот интерфейс (show передаст его subshows). Какие-нибудь расширения GHC для этого? - person imz -- Ivan Zakharyaschev; 08.03.2015
comment
Спасибо, это работает, но я должен добавить объявление типа здесь: (str,rest):_ = reads li :: [(String, String)] и здесь: (char,rest'):_ = reads li :: [(Char, String)]. - person Netsu; 06.11.2015

Вы можете переключиться на использование пакета 'text' для ввода-вывода. Например.

Prelude> :set -XOverloadedStrings
Prelude> Data.Text.IO.putStrLn "hello: привет"
hello: привет

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

Используя файл .ghci, вы можете включить -XOverloadStrings по умолчанию и написать макрос :def, чтобы ввести команду :text, которая показывает значение только через text. Это сработает.

person Don Stewart    schedule 11.04.2011
comment
Похоже, что putStrLn отлично работает для OP. Я думал, что вопрос в том, как переопределить show. - person rampion; 11.04.2011
comment
Хорошая точка зрения! Это действительно конкретно о «шоу», а не о побеге. Возможно, нам следует изменить заголовок. - person Don Stewart; 11.04.2011
comment
Да, вопрос в переопределении show и print, чтобы они не экранировали нелатинские символы. Я не понимаю, почему вы говорите, что дело не в экранировании... Дело в отключении экранирования в print или show, и оба эти слова есть в заголовке: print, unescaped. Какое название будет лучше? - person imz -- Ivan Zakharyaschev; 11.04.2011
comment
@rampion, вы правы: у меня putStrLn работает нормально, но для учебника по хаскелю я бы хотел, чтобы людям не приходилось использовать putStrLn в GHCi, и они по-прежнему могли видеть свои русские строки (обработанные нашей игрушкой). функций для учебника по Haskell). - person imz -- Ivan Zakharyaschev; 11.04.2011
comment
Я всегда получаю это: hello: *** Exception: <stdout>: hPutChar: invalid argument (invalid character). Я запускал его через Cygwin и с помощью платформы Windows Haskell. - person CMCDragonkai; 05.05.2015

Теперь, когда я знаю -interactive-print ghci, это отличная функция. Большое спасибо за то, что написали вопрос и ответы! Между прочим, существующие красивые принтеры, которые я могу найти в Интернете, имеют некоторые угловые случаи, и проблема написать хороший Unicode show оказалось сложнее, чем кажется.

Поэтому я решил написать для этой цели пакет Haskell unicode-show, который (надеюсь, ) хорошо печатает строки в верхнем регистре и составные типы.

С наилучшими пожеланиями, чтобы этот пакет был полезен людям, которые искали этот вопрос и ответ :)

person nushio    schedule 04.02.2016

Что было бы идеально, так это патч для ghci, позволяющий пользователю :set использовать функцию для отображения результатов, отличных от show. В настоящее время такой функции не существует. Однако предложение Дона для макроса :def (с текстовым пакетом или без него) совсем неплохо.

person sclv    schedule 11.04.2011
comment
Означает ли предложение макроса, что нам нужно будет иметь Prelude> :text "hello: привет" на экране в GHCi, т. е. явно введенное имя макроса? Это излишне, потому что приглашение уже предлагает ввести выражение, которое GHCi оценит и распечатает результат. Было бы неплохо, если бы это можно было сделать макросом по умолчанию (для печати результатов) или макросом с нулевым именем поверхности (не нужно ничего вводить, чтобы GHCi использовал его). - person imz -- Ivan Zakharyaschev; 12.04.2011
comment
@imz - Да, макрос по умолчанию был бы великолепен. Я думаю, что это было бы стоящим запросом/исправлением функции, но я не думаю, что это было бы возможно сейчас. Имейте в виду, однако, что макрос может делать что угодно (его тип String -> IO String), поэтому он может постобрабатывать любой вывод, а не только строки или текст. Также имейте в виду, что он может быть всего :p, так что синтаксические накладные расходы, хотя и раздражающие, могут быть минимальными. - person sclv; 12.04.2011
comment
Просто поясню мои слова (не то чтобы я не согласен с чем-то в вашем комментарии): По умолчанию я имел в виду: без поверхностного представления, т.е. невидимый, тот, который используется по умолчанию для постобработки/печати результатов, без синтаксических накладных расходов на все. Для моих целей (вводная демонстрация Haskell) важно, чтобы было задействовано несколько дополнительных понятий, т. е. я не хотел бы, чтобы аудитория увидела неясный макрос и задалась вопросом об этом (и в равной степени я не хотел бы, чтобы аудитория см. escape-последовательности, интересующиеся концепцией экранирования, конкретной формой, используемой здесь, и т. д.). - person imz -- Ivan Zakharyaschev; 12.04.2011