리습 문법에 대해서 전부터 생각하던게 있었는데 밑에 리습 문법 얘기 있길래 생각나서 써봄.
잘 알려져 있는 리습의 대표적인 문법적인 특징은 닫는 괄호가 중첩되는 경우가 많다는 거임.
| 1 2 3 | (foo 1 (bar 2 (baz 3))) |
처럼.
나는 이게 닫는 괄호의 남용이라고 생각함.
C 문법을 잠깐 보자. C에서 세미콜론으로 구분된 statement들은 얼핏 보면 선형적으로 나열되어 있는 것 같지만 사실은 세미콜론이 있을 때 마다 중첩이 깊어지는 형태임.
| 1 2 3 | x = 1; y = 2; z = 3; |
이건 아까 리습 코드하고 전혀 다르게 생겼지만, 사실 문법적으로 굉장히 유사함.
왜냐면 변수의 스코프가 아까 리습코드의 괄호로 결정되는 스코프랑 구조가 똑같음.
x는 1, 2, 3번 줄, y는 2, 3번 줄, z는 3번 줄에서 보임. 각 변수는 그 변수가 선언된 위치 부터 코드의 끝 까지를 스코프로 가짐.
리습 코드도 마찬가지임. 괄호가 시작하는 위치는 제각각이지만 끝나는 위치는 전부 동일함.
그럼 문법적으로 봤을 때 앞서 본 리습 예제를 세미콜론으로 써도 충분히 합리적이라는 결론이 나온다.
| 1 2 3 | foo 1; bar 2; baz 3; |
이렇게 써도 소괄호로 쓴거랑 비슷한 구조로 파싱 가능하다는 소리.
물론 소괄호를 쓰면 임의의 위치에서 스코프를 시작하고 닫을 수 있지만, 세미콜론을 쓰면 무조건 같은 위치에서 일괄적으로 스코프를 닫아야 한다는 문제가 있다. 하지만 이건 필요한 경우에만 명시적으로 괄호를 쓰는걸로 해결 할 수 있음.
| 1 2 3 4 5 6 7 | (foo (bar (baz (qux (quux)))) (quuz (corge))) |
이런 expression을 생각해보자.
| 1 2 3 4 5 6 7 8 9 10 11 | foo { bar; baz; qux; quux; } { quuz; corge; } |
이렇게 쓰면 위랑 비슷한 AST로 파싱할 수 있음.
그러니까 세미콜론 만으로는 스코프가 닫히는 위치를 명시할 수 없지만, 그게 필요한 경우에만 괄호를 써주면 표현력에서도 손해를 보지 않는다.
스코프가 시작되는 위치만 구분하고 끝나는 위치는 일괄적으로 블록의 맨 끝으로 하고 싶을 때에는 세미콜론을 사용하고, 시작과 끝을 모두 명시하고 싶을 때에만 괄호를 쓴다고 생각하면 됨.
세미콜론을 쓰는게 소괄호만 쓰는 것에 비해 더 나은 문법일까? 이건 이견이 있을 수 있지만 나는 세미콜론이 낫다고 생각함.
리습 스타일에서 quuz의 스코프는 5번째 줄의 끝에 닫는 괄호가 몇 개 있는지를 봐야 알 수 있는데, 괄호가 뭉쳐있어서 읽기가 매우 불편하다.
대신 6번 라인의 인덴트를 볼 수도 있긴 하지만, 인덴트는 이상하게 하더라도 문법적으로 문제가 없고, 사이에 낀 블록이 길면 인덴트도 읽기 힘듬.
반면에 비슷한걸 블록 및 세미콜론으로 쓰면 문법 구조를 한눈에 파악할 수 있음.
리습에서 괄호는 function application / call 을 표현하는 문법이고 세미콜론은 sequential한 실행을 표현하기 위한 문법이라는 차이가 있긴 하다.
하지만 이미 function application에 세미콜론과 비슷한 문법을 사용한 선례가 있음.
| 1 2 3 | x = foo 1 $ bar 2 $ baz 3 |
바로 하스켈의 $임. 문법적으로 보자면 $는 그냥 "여는 괄호"랑 비슷함. (사실 $는 binary infix operator라서 좌우로 피연산자가 필요하니 똑같진 않다)
$로 쓴 코드를 괄호로 다시 쓰면 다음과 같음.
| 1 | x = foo 1 (bar 2 (baz 3)) |
사실 $ 뿐이 아님. 하스켈에서는 점차 중첩되는 문법 구조를 그냥 선형적으로 작성하는 경우가 많음.
특히 모나드 문법이 가장 주목할만 함. 왜냐면 단순히 코드 스타일을 떠나서 전용 문법까지 만들었거든.
| 1 2 3 4 | x = foo >>= a -> bar >>= b -> baz >>= c -> qux a b c |
bind operator 를 사용해서 모나딕한 코드를 쓰면 위와 같은 형태가 나옴.
리습 프로그래머는 보통 이것처럼 스코프 중첩이 있을 때 마다 들여쓰기를 하는걸로 알고 있음. (이부분은 내가 리습을 많이 하진 않아서 아닐 수 있음)
하지만 하스켈 프로그래머는 이럴때 들여쓰기를 잘 안함.
| 1 2 3 4 | x = foo >>= a -> bar >>= b -> baz >>= c -> qux a b c |
그냥 이렇게 쓴다.
| 1 2 3 4 5 | x = do a <- foo b <- bar c <- baz qux a b c |
do notation을 쓰면 이렇게 됨.
이건 중첩된 형태의 스코프를 가지는 코드를 선형적으로 보이게 만드는 문법을 의도적으로 넣은거임.
내 결론은 이거임.
닫는 괄호가 몰려 있는 expression은 여는 괄호만으로도 표현 하기에 충분함.
여는 괄호만 쓰는 문법을 지원하면 코드를 훨씬 읽기 편하게 만들 수 있음.
전에 깃갤에서 닫는 괄호를 생략시킨 리습 매크로인지 방언 인지를 봤는데 그거 보고 떠올림. 근데 그 링크를 못찾겠네
그거 인덴트로 괄호추론하는거 아니었냐
이거 역방향으로 하는게 클로저에서 -> 쓰는거네 - dc App
실제로 짤때도 매크로가 있으니까 괄호가 예시처럼 쓰이진 않는데 그 조건부로 닫는것만 해도 꽤 많은듯 - dc App
근데 직접 해보면 아시겠지만 괄호 닫는 거 보지도 않고 신경도 안 씀.. 괄호 맞춰 주는 플러그인이랑 게이색 괄호 쓰면 되고 코드 읽을 때는 어차피 앞쪽만 보게 됨
괄호 쓰는 이유는 그냥 잡다한 문법 거르고 ast 그 자체로 코딩을 해서 매크로질 하려고 하는거지 보통 호모아이코닉이라 하는 것 만족 시키려고.. 매크로질로 얻는 이득이 눈에 보시기에 좋아지는 파이써닉한 문법 설탕들보다 더 크다고 봐서 괄호를 쓴다 생각함
난 괄호색 안써!
맑은고딕으로 코딩하는거랑 다를 바 없네요
이번에는 사랑이 혐오를 이겼다구요
매크로가 장점이긴 한데, 매크로를 쓰기 위해서 설탕을 빼야할 이유는 전혀 없지. 그걸 왜 연결하는지 잘 모르겠음.
코드가 자료구조로만 이루어지니까, 코드를 생성하는 코드를 만드는 게 아주 쉬움. 다른 언어들은 그게 안 되니까 코드를 문자열로 처리하는 걸로 아는데 문자열 조지는 거보다 다양한 자료구조를 조지는 게 편하지 하스켈의 매크로은 어떤 식임 궁금하네
예를 들어서 여는 괄호만 쓰는 문법 설탕을 추가한다고 하면, 어차피 그것도 걍 S-expression 이니까 매크로가 처리해야될 코드는 달라질게 없잖음
매크로에서 코드를 깡 문자열로 처리하는건 진짜 원시적인거고, 매크로 기능을 좀 내세운다 하는 언어는 전부 AST로 다룸. 리습은 그 AST가 S-exp 인 거고
그리고 Homoiconicity랑 코드를 자료구조로 다루는건 조금 다르다고 들음. 하스켈 매크로 시스템 TemplateHaskell 도 AST로 코드를 다루지만 homoiconicity는 없음
다른 언어들은 잘 모르는데 보통 AST라는 특수한 자료구조를 조지는 방식으로 매크로 코드를 작성함? 근데 리슾은 그게 항상 쓰던 코드랑 똑같으니까 편한거고?
ㅇㅇ 해당 언어를 표현하기 위한 AST가 자료구조로 정의가 돼있고 그걸 다룸. 그리고 매크로 코드를 작성하는 언어를 메타언어, 매크로로 다뤄지는 언어를 대상언어라고 하는데 메타언어랑 대상언어가 똑같으면 homogeneous metaprogramming 이라고 부름. template haskell도 homogeneous metaprogramming임
프로그램 코드가 자료구조 표현되서 갖고 놀 수 있는 게 호모아이코닉인데, 이것도 좀 나뉘는 거 같음. 리슾처럼 호모에 온 몸을 불태워서 그냥 어떠한 변환 없이도 코드가 자료구조 리터럴 그자체인거랑(나는 하드-게이라고 부르고 싶음) 다른 소프트-게이 언어들은 그정도까진 아니어도 일단 코드가 자료구조로 조져지면 걍 호모아이코닉이라 불러주는 듯.
줄리아도 지들이 호모아이코닉하다고 하드라고..
글쎄 bar; baz; qux; quux;가 아니라 quux; qux; baz; bar;여야 되는데 이걸 파싱할때 자동으로 뒤집어준다고 하면 기존 연산자의 오퍼랜드 순서가 다 바뀌어야겠지 아님 말고
결론은 파이썬을 하라는 거죠?
파이슨: 커두가 C 매크로 밖에 몰라서 매크로를 안 넣는 언어
갸아악 구아악
일반적으로는 좋아보임
다만 위처럼 code as data, data as code가 가능 한가가 궁금하네
syntax-sugar 형태로 지원하면 파서가 잘 치환해서 평범한 S-exp로 만들 수 있으니 할 수 있다고 생각해
확장자 다르게 만들던가, 특정 키워드 스코프로 통제 가능하면 확실히 좋을듯. 하스켈 장점들을 끌어올수도 있을거 같고.
아 저 sugar로 쓸 때랑 괄호로 쓸 때랑 매크로에서 다르게 처리하고 싶다면 좀 복잡해지긴 하겠다.
신택스 슈가는 무조걸 리스프 코드로 변환되어야 하고, 매크로 내에서는 사용불가라는 조건 또는 rust 같은 매크로를 도입해야할듯
러스트도 매크로가 좋다고는 하다만 리스프보다는 확실히 어렵더라
나도 요즘 매크로 좀 깊게 해보고 싶은데, 추천할만한 언어 있음? scheme, common lisp, clojure 다 조금씩 다른것 같던데
필요하면 그때그때 보는 스타일이라 깊이는 안팠음. 클로져는 거의 안해봤구. 커먼 리스프는 고전적인 리스프 매크로랑 비슷하고, 라켓은 살짝 러스트 느낌도 남. 이런식
https://docs.racket-lang.org/guide/macros.html
갠적으로는 racket 매크로 꽤 좋아함. 보다시피 모나드 구현에 코드 얼마 안들고
https://github.com/tonyg/racket-monad
오버라이드로 아예 다른 방언까지 만들수 있음
https://docs.racket-lang.org/heresy/index.html
아이디어는 좋은데 paredit을 못쓰게 되지 않으려나
첫번째 코드는 클로저에서는 이렇게 쓰면 됨. (->> (baz 3) (bar 2) (foo 1))
아 그렇다더라. 괜찮은 해결책 같음
슈가를 문법으로 추가하는 대신 매크로로 구현하는 느낌 근데 이경우 말고 뒤에 줄줄이 붙는 괄호는 결국 필연적이기 땜에 paredit이랑 rainbow-paren은 그냥 필수