문자 이스케이프 처리하기
이제 Html
에는 자체 소스 파일과 모듈이 있으며, 내보내기 한 함수들만 사용하여 HTML 코드를 만들 수 있습니다.
또한 우리의 메타 언어와 충돌하는 문자를 포함할 수 있는 사용자 입력을 처리해야 합니다.
그러한 문자 중 하나는 HTML 태그를 만드는 데 사용되는 <
와 >
입니다.
이러한 문자를 HTML이 처리할 수 있는 다른 문자열로 변환할 수 있습니다.
다음 Stack overflow 질문에서 처리해야 하는 문자 목록을 확인할 수 있습니다.
escape
함수를 만들어봅시다:
escape :: String -> String
escape =
let
escapeChar c =
case c of
'<' -> "<"
'>' -> ">"
'&' -> "&"
'"' -> """
'\'' -> "'"
_ -> [c]
in
concat . map escapeChar
함수 정의에서 몇 가지 새로운 것을 볼 수 있습니다:
Let
표현식: 이 구문을 사용하여 지역 이름을 정의할 수 있습니다:let
<이름> = <표현식>
in
<표현식>이렇게 하면
<이름>
을in
다음에 나오는 두 번째<표현식>
에서 사용할 수 있습니다.여러 패턴과 일치하는 패턴 매칭: 다른 문자를 일치시키고 문자열로 변환합니다.
_
는 항상 성공하는 "모든 것" 패턴입니다.새로운 함수:
map
과concat
입니다. 이들에 대해 더 자세히 알아보겠습니다.어떤 이유로 인해 이 코드 블록의 구문 강조가 약간 깨졌습니다. 걱정하지 마십시오.
연결 리스트
하스켈에서 연결 리스트(linked lists)는 매우 일반적인 데이터 구조입니다. 그래서 이를 위한 특별한 구문(syntax)이 존재합니다:
- 리스트의 유형은 대괄호로 표시되며 대괄호 안에는 요소의 타입이 있습니다. 예를 들어:
[Int]
- 정수의 리스트[Char]
- 문자의 리스트[String]
- 문자열의 리스트[[String]]
- 문자열의 리스트의 리스트[a]
- 임의의 단일 유형의 리스트 (모든 요소는 동일한 타입이어야 함)
- 빈 리스트는 다음과 같이 작성됩니다:
[]
- 리스트에 요소를 추가하는 것은
:
연산자를 사용하여 수행합니다 (이를cons
라고 함). 이 연산자는 오른쪽 결합성(right-associative)을 가집니다 (예:->
). 예를 들어:1 : []
, 또는1 : 2 : 3 : []
. - 위 리스트는 다음과 같이 작성할 수 있습니다:
[1]
및[1, 2, 3]
.
또한, 문자열은 문자의 연결 리스트입니다 - 문자열은 다음과 같이 정의됩니다:
type String = [Char]
이므로 우리는 문자열을 리스트와 동일한 방식으로 사용할 수 있습니다.
연결 리스트는 편리한 데이터 구조이지만, 모든 경우에 적합한 것은 아닙니다.
특히 공간 효율성이 떨어지고, 데이터 추가, 임의 접근(random access) 등에 느립니다.
따라서 String
을 사용하는 것이 효율적이지 않을 수 있습니다.
그래서 외부 패키지에서 제공하는 Text
라는 다른 문자열 타입을 사용하는 것을 권장합니다.
Text
와 다른 데이터 구조에 대해서는 이후에 다루겠습니다!
리스트에 대한 연산을 패턴 매칭과 재귀(recursion)를 사용하여 구현할 수 있습니다. 이에 대한 자세한 내용은 ADT를 소개할 때 다루겠습니다.
지금은, Data.List 모듈에 있는 다양한 함수를 사용하겠습니다. 그 중에서 map과 concat 함수를 사용하겠습니다.
map
map
을 사용하면 리스트의 각 요소에 함수를 적용할 수 있습니다.
이 함수의 타입 시그니처는 다음과 같습니다:
map :: (a -> b) -> [a] -> [b]
예를 들면:
map not [False, True, False] == [True, False, True]
또는 이전에 정의한 escape
함수처럼, 각 문자열을 이스케이프할 때 사용할 수 있습니다:
map escapeChar ['<','h','1','>'] == ["<","h","1",">"]
하지만 escapeChar
의 타입은 Char -> String
이므로,
map escapeChar ['<','h','1','>']
의 반환 타입은 [String]
이자만, 원하는 것은 String
입니다.
그래서 리스트를 평평하게(flatten) 만들어 줄 concat
함수가 필요합니다.
concat
concat
의 타입은 다음과 같습니다:
concat :: [[a]] -> [a]
이 함수는 리스트의 리스트를 받아서 리스트를 반환합니다.
예제의 경우, concat
은 [String]
을 받아서 String
을 반환합니다.
String
은 [Char]
의 타입 별칭(type alias)이므로, 실제로는 [[Char]] -> [Char]
입니다.
GHCi
우리가 작성한 코드를 빠르게 확인하는 방법 중 하나는 GHCi라는 대화식 개발 환경(interactive development environment)을 사용하는 것입니다.
ghci
를 실행하면 대화식 프롬프트가 열리고, 하스켈 표현식을 작성하고 평가할 수 있습니다.
이를 "Read-Evaluate-Print Loop" (REPL)이라고 합니다.
예를 들면:
ghci> 1 + 1
2
ghci> putStrLn "Hello, world!"
Hello, world!
새로운 이름도 정의할 수 있습니다:
ghci> double x = x + x
ghci> double 2
4
:{
와 :}
로 코드 블록을 감싸면 여러 줄의 코드를 작성할 수 있습니다:
ghci> :{
| escape :: String -> String
| escape =
| let
| escapeChar c =
| case c of
| '<' -> "<"
| '>' -> ">"
| '&' -> "&"
| '"' -> """
| '\'' -> "'"
| _ -> [c]
| in
| concat . map escapeChar
| :}
ghci> escape "<html>"
"<html>"
하스켈 소스 파일은 :load
(또는 :l
로 줄여서) 명령어로 불러올 수 있습니다:
ghci> :load Html.hs
[1 of 1] Compiling Html ( Html.hs, interpreted )
Ok, one module loaded.
ghci> render (html_ "<title>" (p_ "<body>"))
"<html><head><title><title></title></head><body><p><body></p></body></html>"
라이브러리 모듈도 불러올 수 있습니다:
ghci> import Data.Bits
ghci> shiftL 32 1
64
ghci> clearBit 33 0
32
표현식의 타입을 알고 싶다면 :type
(또는 :t
로 줄여서) 명령어를 사용할 수도 있습니다:
λ> :type escape
escape :: String -> String
ghci
를 종료하려면, :quit
(또는 :q
로 줄여서) 명령어를 사용합니다:
ghci> :quit
Leaving GHCi.
GHCi는 빠른 실험과 탐색에 유용한 도구입니다.
위에서 몇 가지 예를 보았습니다 - escape
함수에 문자열 "<html>"
을 전달하면 "<html>"
라는 문자열을 반환합니다.
이 문자열은 브라우저에서 HTML 태그 대신 <html>
로 렌더링됩니다.
만약 특정한 함수가 무엇을 하는지 이해하기 어렵다면, GHCi에서 테스트해보세요. 다양한 입력을 넣어보고, 예상한 결과와 일치하는지 확인하세요. 구체적인 예제를 실행하는 것은 코드를 이해하는 데 많은 도움이 될 수 있습니다!
GHCi에 대해 더 자세히 알고 싶다면, GHC 사용자 가이드에서 더 자세한 소개를 찾을 수 있습니다.
이스케이프
현재 우리가 작성한 라이브러리는 오직 다음 항목만 지원합니다:
- 페이지 제목
- 문단
- 제목
더 진행하기 전에, escape 함수를 적용해 HTML 생성을 안전하게 만들려고 합니다.
escape 함수를 적용해보세요.
정답
수정된 Html.hs
hello.hs
파일에서 유효하지 않은 HTML을 제공하면 제대로 동작하는지 확인해보세요!
이제 우리는 작은 HTML 라이브러리를 안전하게 사용할 수 있습니다. 그러나 사용자가 예상하지 못한 유효한 사용 사례, 예를 들어 순서 없는 목록을 추가하려는 경우에는 어떻게 해야 할까요? 우리는 라이브러리를 확장하는 것을 완전히 막고 있습니다. 다음에는 이에 대해 이야기 해보겠습니다.