3P by GN⁺ 3일전 | ★ favorite | 댓글 1개
  • 왼쪽에서 오른쪽으로 프로그래밍하는 방식은 코드를 입력하는 즉시 프로그램이 유효한 상태를 유지하며, 이로 인해 에디터의 자동완성 등 도구 지원이 극대화됨
  • Python의 리스트 내포는 선언되지 않은 변수와 타입 추론의 부재로 자동완성 기능을 방해함
  • Rust나 JavaScript는 프로그램을 왼쪽에서 오른쪽으로 자연스럽게 구성할 수 있어 변수 사용과 메서드 탐색이 더 직관적임
  • C와 Python의 함수형 스타일은 함수명이나 구조의 비발견성으로 인해 효율적인 코딩 경험을 저해함
  • 복잡도가 높은 로직에서는 왼쪽에서 오른쪽으로 전개되는 코드가 더 읽기 쉽고, 유지보수 및 확장성이 우수함

왼쪽에서 오른쪽으로 프로그래밍하기

코드는 입력하는 즉시 유효해야 함


Python 리스트 내포의 한계

  • Python의 리스트 내포 구문 words_on_lines = [line.split() for line in text.splitlines()]은 선언되지 않은 변수(line)에 접근해야 하므로, 에디터가 자동완성이나 타입 추론을 제대로 제공하지 못하는 문제 발생
  • 코드를 부분적으로 입력하는 과정에서
    • words_on_lines = [line.sp처럼 입력하면 에디터는 line의 타입을 알지 못해 메서드를 추천할 수 없음
    • 변수명 오타(lime 등)와 같은 잠재적 오류 탐지도 어렵게 됨
  • 올바른 추천을 받으려면 미완성 코드를 작성해야 하며, 그 과정이 비직관적이고 불편함을 초래함

Rust에서의 왼쪽에서 오른쪽 구성

  • Rust 예제(let words_on_lines = text.lines().map(|line| line.split_whitespace());)는
    • 익명 함수의 선언과 함께 변수(line)가 처음 등장하는 순간 선언으로 간주되어, 즉시 자동완성 및 메서드 추천이 가능해짐
    • 실제로 split_whitespace라는 메서드도 자동추천된 덕분에 쉽게 찾을 수 있었음
  • 이 방식은 프로그램이 항상 부분적으로라도 유효한 상태를 유지하므로, IDE나 에디터가 실시간으로 코딩을 지원할 수 있음

점진적 공개(Progressive Disclosure)와 API 사용성

  • 점진적 공개(Progressive Disclosure) 는 사용자가 필요한 만큼만 복잡도를 경험하는 설계 원리로, 프로그래밍에도 적용 가능함
    • 예: 이미지를 추가할 때만 관련 옵션이 나타나는 워드프로세서의 UX와 유사함
  • C 언어는 이런 지원이 부족함
    • FILE *file에 관련된 모든 함수가 file.로 탐색되지 않으므로, 함수명의 패턴(fread, fclose 등)을 외워야 하고 기능을 발견하기 어려움
    • 반면 이상적인 언어라면 file.을 통한 메서드 추천으로 관련 기능을 쉽게 점진적으로 발견할 수 있음

함수 및 메서드 발견성의 차이

  • Python의 map(len, text.split())과 JavaScript의 text.split(" ").map(word => word.length) 예시 비교
    • Python에서 len, length, size 등 함수명이 예상되지 않아 여러 시도를 해야 실제 동작을 알 수 있음
    • JavaScript에서는 word. 뒤에 .l 만 입력해도 에디터가 length 등 메서드를 제시하여 발견성이 높음
    • map 같은 고차 함수도 실제 반환값과 데이터 타입이 즉각적으로 명확하게 드러남

복잡한 로직일수록 구조적 작성의 장점

  • 복잡도가 높은 로직(filter, lambda가 중첩된 긴 Python 코드)의 경우
    • 코드의 시작과 끝을 반복적으로 확인해야 하며, 조건식이나 괄호 매칭 등에서 가독성 저하와 이해의 어려움이 발생함
  • 동일한 로직의 JavaScript 버전에서는 코드를 위에서 아래로, 왼쪽에서 오른쪽으로 순차적으로 읽고 이해할 수 있음

핵심 원칙

코드는 입력하는 순간마다 유효해야 함

  • text 단독 입력에도 프로그램이 유효한 상태 유지
  • text.split(" ")까지 작성해도, 이후 .map(word => word.length)까지 이어서 입력할 때도, 전체적으로 항상 중간 상태가 유효함
  • 이러한 코딩 패턴은 에디터의 실시간 지원 가능성을 높이고, REPL 환경에서는 즉시 결과를 확인할 수도 있음

결론

  • API와 언어 디자인은 코드를 왼쪽에서 오른쪽으로 자연스럽게 입력하며 중간 단계마다 유효한 프로그램을 만들 수 있도록 지원해야 함
  • 좋은 API 설계가 이러한 코딩 경험 개선의 핵심임
Hacker News 의견
  • SQL의 단점 중 하나는 쿼리문이 FROM이 아닌 SELECT로 시작한다는 점임, 그래서 어떤 엔티티(테이블)를 다루는지 바로 파악하기 어렵고, 스마트 에디터에서 쿼리 작성을 더 효율적으로 도와주는 데도 방해가 됨, FROM -> SELECT -> WHERE 순서로 가는 게 더 자연스러움, 특히 SELECT절에서 컬럼 이름을 정하고 WHERE에서 이를 참조하기 때문에 더 그렇다고 생각함, 사실 SELECT * FROM table 대신 FROM table만 써도 SELECT절은 생략 가능할 것이라 봄, 이런 불만 때문에 잔소리쟁이 노인처럼 들릴지 몰라도 그냥 내 개인적 그리움임
    • PSQL과 PRQL은 실제로 FROM이 먼저 오는 쿼리 순서를 사용함, BigQuery에도 파이프/화살표 문법이 최근 추가되었고, DuckDB 커뮤니티 익스텐션도 있으니 추천함 DuckDB - PSQL, DuckDB - PRQL
    • SQL이 이렇게 쓰이는 이유는 관계형 대수 기초에서 투영(Projection)을 항상 먼저 적기 때문임, 그래서 표준에 따르면 WHERE에서 컬럼 별칭을 쓸 수 없음, 왜냐하면 selection(WHERE)이 projection(SELECT) 전에 일어나기 때문임, 참고로 MySQL 8에서는 TABLE <table> 이란 문법도 있으니 참고할 만함
    • 실제로 대부분의 SQL 엔진 내부 처리 순서는 FROM -> WHERE -> SELECT 순서임, 그래서 SELECT에서 정의한 컬럼 별칭이 GROUP BY, HAVING, ORDER BY에는 쓰이지만 WHERE에서는 쓸 수 없음
    • C#에서는 SQL로 컴파일되는 DSL(LINQ-to-SQL)도 FROM이 먼저 오는 구조임, 그리고 IDE에서 다른 절을 작성할 때 자동 완성 기능 덕분에 필드 제안을 바로 받을 수 있어서 이런 구조가 좋다고 생각함
    • Kusto라는 Azure의 데이터 분석 쿼리 언어도 파이프를 쓰는 유사한 형태임 Kusto 쿼리 소개, .NET의 LINQ 스타일도 마찬가지임, 솔직히 SQL에도 FROM으로 시작하는 변형이 좀 더 적극적으로 도입되어야 하고, 그게 어려운 일도 아니라 생각함, 사용성 향상을 위한 시도가 부족하다고 봄
  • 파이썬이 왜 이렇게 사랑받는지 이해하기 어렵다고 생각함, 두 사람 이상이 작업하면 언어가 한없이 고통스러워짐, 글쓴이가 지적한 부분은 빙산의 일각일 뿐임
    • 사람들이 Lisp류 언어에 몰리지 않는 이유와 비슷하다고 생각함, 수학적 엄밀함이 곧 가독성을 의미하지는 않음, 파이썬의 list/dict/set comprehensions는 타입이 정해지는 for 루프와 마찬가지임, 모두가 파이썬의 타입이 헐렁하다고 걱정하지만 리턴 타입을 명확하게 정해주는 유일한 구문(리스트 컴프리헨션)이 표적이 되는 건 이상함, Rust를 포함한 대부분의 다른 언어들에서도 "from iter as var" 순서는 아님, 또 각 언어들의 함수 호출 문법을 비교하는 것도 재미있음(파이썬에서도 functools.map이 있듯이)
    • 무언가를 이해하지 못한다고 해서 그게 미덕이 되는 것은 아니라고 봄, 파이썬이 사랑받는 이유엔 분명 뭔가가 있음, 물론 단점도 분명하지만, 그것만으로는 의미가 없음, 장단점을 전체적으로 비교해봐야 하고, 다른 언어와도 그렇게 비교해야 함
    • 나도 파이썬을 좋아함(단, 소규모 팀, 짧고 수명이 짧은 프로그램 기준), 정적 타입이 없어서 구현이 빠르지만 강한 타입 시스템 덕분에 완전히 망가지진 않음, 이 점 때문에 데이터 과학에서 인기가 많다고 봄, 탐색을 할 때 매우 편함, 반면 장기적으로 여러 팀이 유지보수 하거나 대규모 프로그램엔 분명 단점이 있음, 결국 만능 언어는 없고 적어도 "시도하고 빠르게 갈 수 있는 언어(Soft)"와 "오랜 기간 관리하기 좋은 언어(Hard)" 두 가지는 필요함
    • 나도 예전엔 위 의견에 완전히 동감했지만, 타입 주석과 타입 검사 덕분에 다른 사람들이 쓴 파이썬 코드와 협업이 훨씬 수월해졌음, 여전히 대규모 프로젝트까지는 아니라고 생각하지만, 타입이 붙으면서 파이썬은 내가 가장 좋아하는 스크립트 언어가 되었음
    • 나도 공유 코드베이스에선 리스트 컴프리헨션 같은 걸 피하고 아주 단순한 파이썬 스타일을 지향함, "한 가지 방식만 존재"해야 한다고 알려진 언어지만 실제로는 너무 많은 방식이 공존함, 리스트 컴프리헨션은 개인적으로 재밌고 만족스럽지만, 모두가 한 길로 가야 한다면 이 문법은 없어야 한다고 봄
  • "프로그램은 타이핑하는 즉시 유효해야 한다"는 주장에 공감은 하지만, 현실에서는 코드를 항상 왼쪽에서 오른쪽, 한 줄씩 순차적으로 작성하지 않음, 중간에 다른 부분을 먼저 작성하거나 변수 선언을 나중에 하는 경우도 많음, 예를 들어 변수를 사용해놓고 한참 뒤에 선언할 때도 있음
    • 코드는 한 번 작성되고 수십, 수백 번 읽힌다는 점에서, 순차적으로 읽을 수 있는 코드가 점프를 요하는 코드보다 훨씬 읽기 편하다고 생각함
    • 사실 이 논의는 글의 핵심에서 살짝 벗어나긴 하지만, 흥미로운 시각임
    • 완전히 동감함, 새 파일 만들 때만 코드를 처음부터 순서대로 씀, 필드 추가할 땐 굳이 클래스 정의부터 가기보다 바로 그 필드를 쓰는 코드부터 만듦, 조건문 개선할 때도 잠시 동안은 유효하지 않은(에러 나는) 상태가 되는 경우가 많음
    • 이 의견에도 동의하지만, 이것과 연결된 중요한 원칙은 "너 아직 코딩 다 안 끝냈으니 컴파일 자체를 못 하게 막아버리는" 구조는 너무 과함, 에러는 비차단적이어야 하는데, 일부 언어는 미완료 코드를 아예 막아버림(예: 미사용 변수, 누락된 return 등)
    • IDE가 내가 실제로 코드를 작성하는 순서를 잘 모른다고 느낄 때가 종종 있어서 약간 불편함
  • 일부 IDE에서는 코드 템플릿 기능으로 약어를 입력하면 코드 구조로 확장하고, 탭으로 각각의 플레이스홀더를 채워나가는 기능이 있음, 이때 탭 이동 순서를 반드시 왼쪽에서 오른쪽이어야 할 필요는 없어서 예를 들면 {3} for {2} in {1} 같은 순서도 가능함, 이런 도구들은 "읽기 좋은 문법"과 "타이핑하기 쉬운 문법" 사이의 타협점을 제공함, 나는 툴링을 활용해서라도 읽기 좋은 문법을 우선하는 쪽에 손을 들어줌, 꼭 "for-in" 구조만 고집할 이유는 없다고 생각함
  • 요즘 Hacker News에서 합의된 분위기는 파이썬이 pipe 연산자를 빠뜨렸다는 것임, 나는 Mathematica에서 R로 넘어오면서 파이프의 가치를 빨리 깨달았음, 데이터 사이언스에서 단계별 데이터 변환 코드를 쓸 때 정말 직관적이고 읽기 쉬움, 파이썬이 여러 분야에서 쓰이지만 데이터 분석 외 다른 문맥에서도 파이프가 이점이 있을지 궁금함, 왜 파이썬이 파이프를 도입하지 않았는지 이해하려고 함
    • 파이프 연산자에서 한 걸음 더 나아가면, reverse assignment도 해볼 만하다고 생각함, 'let foo = ...'처럼 결과를 변수에 할당하는 대신 '... =: foo' 같은 형식도 써보고 싶음
    • R(특히 tidyverse R)의 파이프 연산자는 나에게 가장 중요한 "킬러 앱"임, 데이터 작업이 이렇게 쉽고 즐거운 언어 다시 없다고 생각함, 예를 들어 쿠키 레시피를 bake(divide(add(knead(mix(flour, water, sugar, butter)),eggs),12),450,12) 이런 식으로 중첩해가는 대신, mix(flour, water, sugar, butter) %>% knead() %>% add(eggs) %>% divide(12) %>% bake(temp=450, minutes=12) 로 파이프 쓰면 훨씬 쉽고 보기 좋음
    • 파이썬 pandas에서 pipe 문법을 쓰면 아래처럼 되고
      result = (df
       .pipe(fun1, arg1=1)
       .pipe(fun2, arg2=2)
      )
      
      R에서는
      result <- df |>
       fun1(., arg1=1) |>
       fun2(., arg2=2)
      
      둘 다 읽기 괜찮은데, R은 데이터프레임 밖에서도 파이프가 더 잘 동작하는 게 장점임
  • 이 논쟁은 FP(함수형) vs OOP(객체지향) 언어 논쟁, vim과 emacs 논쟁처럼 거의 종교 전쟁에 가까움, vim은 연산자가 먼저, emacs는 선택순서가 먼저임, 영어처럼 "영어식으로 읽히는" 언어는 대개 동사가 앞에 오는 구조임(Lisp/Scheme이 그렇고), 반면 독일어, 타밀어처럼 동사가 맨 뒤에 오는 언어는 OOP 스타일(명사 먼저)과 어울림, 예를 들어 타밀어로는 "water drink" 순이고 영어는 "drink water"임, 그래서 vim을 더 편하게 느끼는 사람이 있을 수 있음, 각각의 스타일이 더 낫다기보다는 도구/사람 성향에 맞게 만들어지기도 하고, 요즘엔 언어 모델로 웬만한 건 다 가능하다고 봄
    • "영어처럼 읽히게 설계한다면 동사 먼저냐"에 대해, 명령형 언어면 맞겠지만, 선언형 언어에서 영어처럼 읽히게 하면 주어 먼저임
    • "독일어는 동사가 항상 뒤에 오냐"에 대해, 사실 간단한 문장은 동사가 두 번째에 옴("I drink water" → "Ich trinke Wasser"), 완전히 문장 끝에 오는 것도 아님
    • vim에서 연산자가 먼저라는 얘기에 대해, 사실 Kakoune은 그 반대로 동작하고, 이 방식이 훨씬 더 논리적이라고 생각함 Kakoune에 대한 설명
  • 한편, 파이썬의 "from some_library import child_module" 문법은 매우 직관적임, JS에서는 "import { asYetUnknownModule } from SomeLibrary"과 같은 구조라서 훨씬 덜 직관적으로 느껴짐
    • JS에서는 namespace import로 다음과 같이 쓰면
      import * as someLibrary from "some-library"
      someLibrary.someFunction()
      
      실제로 IDE 오토컴플리션이 잘 동작해서 좋다고 생각함 MDN namespace import 설명
    • "from" 키워드에 집착하는 이유를 의문스럽게 생각함, 그냥
      import SomeLibrary {
        asYetUnknownModule
      }
      
      이런 식으로 하면 된다고 봄
  • ReScript는 바로 이 이유 때문에 API를 data-last에서 data-first로 바꿈, 훌륭한 타입 추론 덕분에 거의 항상 정확하고 타입에 맞는 자동 완성이 제공돼서 개발 경험이 매우 좋음, 물론 참조 없이 함수를 선언하면(타입을 알 수 없으니) 여전히 문제가 있지만, 타입을 추가하거나 먼저 호출하면 해결됨, 관련 블로그 글도 추천함 Data-first와 data-last 비교
  • 이런 시각을 계속 주장해왔는데, 실제로 Ruby가 나에게 훨씬 쉽게 느껴진 이유와도 연결됨, 특히 나는 파이썬도 루비도 프로덕션 수준에서 깊게 써본 적 없지만, 파이썬이 왜 그리 널리 설치되고 쓰이기 시작했는지 이해가 잘 안 됨, 루비도 단점이 없진 않지만, 스크립트 작성에서 파이썬이 가진 복잡한 변화까지 직면하는 사람 별로 없는 듯, 적어도 루비는 지난 10년간 큰 버전 충돌 사건은 없었음
  • 전체적으로 기사에서 지적한 부분에 전적으로 동의함, 맥락이 앞에 오고 왼쪽에서 오른쪽으로 읽히는 구조가 LLM이나 오토컴플리션에 더 잘 맞을 거라 생각함, 다만 예시 코드는 len(list(filter(lambda line: all([abs(x) >= 1 and abs(x) <= 3 for x in line]) and (all([x > 0 for x in line]) or all([x < 0 for x in line])), diffs))) 처럼 쓰는 것보다는, NumPy array를 활용하는 편이 메모리 상에 리스트를 새로 만들 필요 없고, 줄 전체를 한번에 다루기에도 훨씬 좋음, 예를 들어
    sum(1 for line in diffs
      if ((np.abs(line) >= 1) & (np.abs(line) <= 3)).all()
        and ((line > 0).all() or (line < 0).all()))
    
    이쪽이 훨씬 "왼쪽에서 오른쪽" 잘 반영됨
    • numpy 버전도 여전히 다소 암호 같긴 한데("line > 0"은 괜찮지만 브로드캐스팅 규칙이 복잡해질 수 있음), 저자가 든 자바스크립트 예시나 C#, Java, Scala처럼 타입 엄격한 언어의 컬렉션 API가 더 깔끔함, 내 취향은 코틀린인데 아래처럼 쓸 수 있어서 좋아함
      diffs.countIf { line -> 
        line.all { abs(it) in 1..3 } and ( 
          line.all { it > 0} or
          line.all { it < 0}
        )
      }