Uploaded avatar of xavdid

#12in23을 완주한 사람의 회고

@xavdid
2년 초과 전

이 글은 원래 David의 웹사이트에 게재되었으며, 허락을 받아 이곳에 다시 게재합니다

지난 1월, Exercism은 12in23이라는 새 프로그램을 발표했어요. 참가자들에게 2023년 한 해 동안 12개의 새로운 프로그래밍 언어를 접해 보라는 도전이었죠. 매달 주제가 있었고("Analytical April"이나 "Object Oriented October"처럼), 도전해 볼 특정 언어들을 소개했어요. 저는 새로운 것을 배우는 걸 좋아하고 (프로그래밍) 언어에 꽤 빠져 있기도 해서, 한번 해보기로 했어요. 12개 언어, 12개월!

이제 한 해가 거의 끝나 가는데, 이 프로젝트가 어떻게 진행됐는지 정말 만족스러워요. 12개의 새로운 언어를 성공적으로 접해 봤고, Exercism 커뮤니티에서 멋진 사람들을 만났으며, 그 과정에서 멋진 오픈 소스 기여도 몇 개나 해냈어요! 이 글에서는 그것들을 하나씩 훑어보고, 각각에서 무엇을 얻었는지 이야기해 볼게요.

언어 고르기

그 경험을 최대한 잘 활용하기 위해, 그해를 위한 몇 가지 원칙을 세웠어요:

  1. 언어는 저에게 완전히 새로운 것이거나, 최소한 많이 배우고 있다고 느낄 만큼 충분히 낯선 것이어야 해요.
  2. 선택한 언어는 앞으로 더 배워 나가는 데 (잠재적으로) 실용적이어야 해요. 이 프로젝트는 그저 재미를 위한 것이었지만, 적어도 어느 정도는 유용한 것을 배우는 데 시간을 쓰고 싶었어요.
  3. 사용하는 모든 언어에 대해 로컬 도구와 VSCode 플러그인을 모두 설치했어요. 최대한 많은 타입 힌트와 인텔리센스를 갖춘 채로, 비슷한 조건에서 언어들을 비교하고 싶었거든요. 대학 때는 린팅이나 자동 완성 없이 Sublime Text로 모든 프로그래밍 숙제를 했어요. 그런 도구를 쓰면서 프로그래밍을 배우면 그것들에 너무 의존하게 되어 좋은 프로그래머가 되지 못할까 봐 걱정했죠. 그런데 오히려 반대였어요. 도구에 인지 부담을 더 많이 넘길수록, 눈앞의 실제 문제에 더 많이 집중할 수 있어요. 기억하지 말고, 찾는 법을 기억해요.

그럼 시작해 볼까요!

January (주제 없음)

1월이 시작될 무렵, Exercism 팀은 아직 월별 주제를 고르는 중이어서, 그달의 언어는 마음대로 고르는 것이었어요. 방향이 없어서, 저는 Go로 한 해를 시작했어요. 2022년 중반에 이 언어를 속성으로 배운 적이 있었지만, 그 뒤로는 많이 써 보지 않아서 전혀 능숙하다고 느껴지지 않았어요.

Go는 흥미로운 언어예요. 엄격한 컴파일러 덕분에 프로그램은 반드시 올바르게 되고, 컴파일러가 안전하다고 판단할 때까지는 한 발짝도 나아가지 못해요.1 오류 처리를 장황하게 하는 방식 덕분에 절대 놀랄 일이 없어요(그 대신 if err != nil { return err }를 정말 정말 많이 쓰게 되지만요). 어려운 일을 쉽게 만들어 주는 데 능해요(채널을 통한 병렬 처리처럼). 반면 쉬운 일을 어렵게 만들기도 하고요(문자열 조작). 표준 라이브러리가 탄탄해서, 대부분의 작업을 서드파티 모듈 없이 해낼 수 있어요. 생태계의 많은 부분(포매팅, 설치, 빌드 등)이 퍼스트파티로 go 명령에 내장되어 있는 점이 좋아요. 이 언어에는 비판하는 사람들도 있지만, 정확성과 유지보수성이라는 목표는 대체로 달성한다고 생각해요.

그렇게까지 즐겁게 써서 첫 번째 선택으로 집어 들 정도는 아니지만, 성능에 민감한 프로그램을 만들 때는 도구함에 있으면 좋은 훌륭한 도구예요. 셸 프롬프트에 중첩 경로를 표시할 때처럼요.

Functional February

2월은 함수형 언어로 곧바로 깊은 물속에 뛰어들었어요. 함수형 언어는 더 흔한 명령형 프로그래밍 언어에서 갈라져 나온 수학적인 갈래죠. 함수형 언어는 (부작용이 없는) "순수" 함수로 유명해요. 저는 Elixir를 골랐는데, 주로 친구 Caleb이 Advent of Code에 이걸 써 보고 극찬했기 때문이에요.

Elixir를 다루는 시간이 꽤 즐거웠어요. Elixir는 Ruby에서 영감을 받았어요(말이 되죠. 만든 사람인 José Valim은 Rails 핵심 기여자였으니까요). 메서드 체이닝 같은 함수형 개념을 표현하기가 간단했어요. 그걸 쉽게 만들어 주는 온갖 문법적 편의 기능이 좋았어요. 파이프 연산자(|>)처럼요:

foo(bar(baz(new_function(other_function()))))
# becomes
other_function() |> new_function() |> baz() |> bar() |> foo()

매크로, 즉 코드를 작성하는 코드를 다뤄 본 것도 처음이었어요. Elixir 프로그램은 그 자체로 유효한 Elixir 코드인 AST로 표현될 수 있어서, 다른 유효한 코드를 만들어 내는 코드를 작성하기 쉬워요. Elixir가 쉽게 만들어 준 정말 멋진 개념이에요. 함수가 인자의 형태에 대해 패턴 매칭을 할 수 있어서, 함수 호출이 적절한 구현으로 연결될 수 있는 방식도 마음에 들었어요:

defmodule TuplePrinter do
  def print({a}) do
	IO.puts("single")
	IO.puts(a)
  end

  def print({a, b}) do
	IO.puts("double")
	IO.puts(a)
	IO.puts(b)
  end
end

TuplePrinter.print({1})
TuplePrinter.print({2, 2})

# single
# 1
# double
# 2
# 2

이런 기능은 아주 훌륭하거나, 아니면 코드를 완전히 스파게티로 만들어 버리는 종류인 것 같아요. 어느 쪽이든, 멋진 개념이었어요!

Elixir는 Erlang의 BEAM 가상 머신 안에서 실행된다는 이점도 있어요. 덕분에 상호작용할 수 있는 큰 생태계를 갖게 되죠. 동시성 처리에 뛰어나고, 사랑받는 Phoenix 웹 프레임워크의 핵심이에요.

당장 Elixir를 써야 할 필요는 없지만, 다루는 게 정말 재미있었고 언젠가 다시 들여다볼 의향은 충분히 있어요. 게다가 익숙한 문제를 낯선 방식으로(그러니까 재귀적으로) 풀어 보는 것도 흥미로운 도전이었어요.

Mechanical March

3월은 기계어로 컴파일되는 "시스템" 언어에 초점을 맞췄어요.

선택지 중에서 제가 관심 있는 언어는 Go가 유일했어요.2 눈치 빠른 독자라면 제가 이미 한 달을 Go로 보냈다는 걸 알아차릴 텐데, 그래서 다시 하는 건 12개에 포함되지 않았어요. 뭐, 제가 1월에 Go를 골랐을 때는 아직 주제를 운영한다고 발표하지 않았어서, 제가 스스로 궁지에 몰아넣고 있다는 걸 몰랐어요.

Bun이 나온다는 걸 알았더라면 아마 Zig을 시도했을 텐데, 아쉽게도 (아직) 미래를 볼 수는 없었어요. 그래서 더 끌리는 선택지가 없는 상황에서, 한 해 뒤쪽 어딘가에서 한 언어를 두 번 해야 한다는 걸 알고도 Go로 한 달을 더 보냈어요.

Analytical April

4월은 데이터 과학에서 인기 있는 언어에 관한 달이었어요. 저는 Python에 너무 익숙했고, 대학 때 통계 수업에서 R을 했었는데(별로 좋아하지 않았어요), 그럼 Julia로 가자, 했죠!

다뤄 보니 즐거웠는데, 대부분 Python과 너무 비슷하게 느껴졌기 때문이었어요. 조금 불안하기도 했어요. 캐나다에 있는 미국인 같은 기분이랬죠. 모든 게 아주 익숙하게 느껴지는데, 꼬집어 말하기 어려운 방식으로 아주 조금 어긋나 있어요. 그러다 갑자기 누군가 2달러 동전을 건네주면(혹은 행렬 연산에 정말 잘 맞는 함수를 보면), 더 이상 캔자스에 있지 않다는 걸 깨닫게 돼요.

가장 눈에 띈 것은 Julia의 타입 시스템이었어요. Python의 타입 시스템처럼 (선택적으로) 타입을 표기하는데, 인자가 선언된 타입과 일치하는지 보장하는 런타임 검사도 있었어요. Python의 시스템이 도구에 연결되는 것과 방해되지 않는 것 사이에서 적절한 균형을 잡는다고 생각하지만, 타입이 잘못된 함수에 대한 Julia의 런타임 오류도 유용했던 건 인정해요.

결국 Julia는 멋졌지만, 앞으로 필요할 것 같지는 않아요.

Mindshifting May

5월은 아주 특이한 일을 하는 언어들을 조명하면서 "새로운 것을 시도하라"는 데 더욱 힘을 실었어요. 저는 이 기회에 늘 인기 있는 Rust를 시도해 봤어요. 솔직히 말하면, 왜 이렇게 인기인지 이해가 돼요.

악명 높은 빌림 검사기에 익숙해지는 데는 확실히 시간이 걸리지만, 덕분에 프로그램을 더 신중하게 생각하게 되는 점이 좋았어요. 컴파일러는 확실히 엄격했지만, 오류 메시지는 문제를 고치도록 돕는 데 그 이상이었어요. 첫 주에 특별히 생산적이었다고는 못 하겠지만, 적어도 학습 곡선의 꼭대기는 보이는 것 같아요.

Rust의 패키지 관리자인 cargo도 특별히 언급할 만해요. 서드파티 패키지는 하나도 설치하지 않았지만, 빌드, 테스트, 포매팅 기능이 훌륭했어요. VSCode 확장도 마찬가지였어요. Rust처럼 정적 타입 언어에서 기대할 만한 편의 기능을 다 갖추고 있었어요. 좋은 개발자 경험이 정말 모든 것을 좌우해요.

구현 수준에서는 아주 다르지만, 제가 쓰는 용도, 즉 프로그램을 아주 빠르게 실행하는 데 있어서는 Rust가 Go와 비슷하게 느껴졌어요. 제가 자주 쓰는 언어의 많은 도구들이 성능 특성 때문에 Rust로 눈을 돌리기 시작했어요. 그래서 앞으로 더 많이 보게 될 것 같아요(제가 직접 Rust를 쓰지 않더라도요).

Summer of Sexps (6월)

6월은 S-표현식의 달이었어요. 리스프에서 흔한 구문 형식이죠. 저는 JVM에서 실행되는 함수형 언어인 Clojure를 골랐어요.

몇 년 전에 Clojure를 조금 써 본 적이 있었어요. 학교를 막 졸업하고 첫 직장에서 업무에 중요한 일일 스크립트의 유일한 유지보수 담당자가 되었어요. 말할 것도 없이, 힘든 시기였어요. 나이도 먹고 지혜로워진 지금은 좀 더 다가가기 쉬워졌는지 궁금했어요.

기쁘게도 그렇더라고요! 2월의 함수형 경험이 재귀적으로 사고하는 데 도움이 되었고, 막상 파고들어 보니 문법도 그렇게 나쁘지 않았어요. 더 큰 프로젝트에서 쓴다면 JVM 상호운용성도 유용하겠죠.

대안이 있는 상황에서 제가 Clojure를 쓸 일은 없을 것 같지만, 그렇다고 전혀 불쾌한 경험은 아니었어요.

사이드 퀘스트: Universal Test Runner!

몇 년 동안 저는 현재 디렉터리에서 단위 테스트를 실행하기 위해 작은 bash 함수 하나를 써 왔어요. 이 새로운 언어들을 다루면서, 편의를 위해 거기에 줄을 계속 추가하게 되더라고요. t를 실행하는 걸 기억하는 게, 언어별 테스트 명령을 매번 다시 익히는 것보다 훨씬 쉬웠거든요.

필요한 로직이 제가 bash로 편하게 다룰 수 있는 수준을 넘어서자, 6월에 시간을 내서 이 프로젝트를 독립된 것으로 분리했어요. 바로 Universal Test Runner예요.

Exercism 포럼에 공유했더니 좋은 반응을 얻었어요. 반응이 너무 좋아서, 비슷한 기능을 Exercism CLI 자체에 넣기로 했어요(이건 Go로 작성되어 있는데, 마침 제가 운 좋게 막 다시 훑어본 주제였죠). 그래서 그해 후반기에는 exercism test를 실행해서 그달 언어의 테스트 스위트를 돌릴 수 있었어요(Universal Test Runner에서 기본 지원하는 명령이기도 해요).

이 과정에 대해 더 알고 싶다면, 출시할 때 훨씬 자세히 써 두었어요.

아무튼, 계속해 볼게요!

Jurassic July

7월은 오래된 언어들을 소개했어요. 실용성 면에서는 이번 달 선택지가 꽤 빈약했어요. 저는 오래되고 존경받는 COBOL로 시작했어요. 아직도 많은 핵심 인프라를 돌린다고 들었거든요. 그런데 8월 초 결혼식이 다가오고 있어서, 저에게 이렇게 낯선 언어를 앉아서 배울 여유가 없었어요. 그래서 대신 가장 덜 나빠 보이는 선택지로 Visual Basic으로 바꿨어요.

여기에 대해 할 말은 별로 없어요. 언어는 조금 장황해 보였지만 쓰기는 충분히 쉬웠어요. 제가 이해하기로는 Windows에서 UI 개발을 위해 설계된 것이라, 작은 연습 문제만 풀어서는 전체를 제대로 파악하기 어려웠어요.

Appy August

8월은 앱을 만드는 언어들로 가득했어요. 당연하게도 이번 달은 선택지가 많았어요. 저는 Swift를 골랐어요. Apple 제품을 많이 쓰는 사람으로서, Apple이 직접 만든 언어는 저와 꽤 관련이 있어요. 완전히 처음은 아니었어요. 2016년에 순전히 Swift로만 작성한 iOS 앱 하나를 출시했거든요. 그렇지만 그 뒤로는 이 언어를 만져 본 적이 없고 많이 발전했으니, 그래도 해당된다고 봤어요.

다루기가 이렇게 쉬워서 기분 좋게 놀랐어요. 여기 나온 다른 많은 언어와 달리, Swift는 꽤 최신이에요. 2014년에 처음 나왔고, 현대 언어 설계의 교훈을 분명히 잘 흡수했어요. 퍼스트파티 패키지 관리자, 옵셔널 체이닝, 일급 함수, 그리고 이치에 맞는 문자열 보간을 갖추고 있어요. Xcode를 쓰지 않고도 읽고 쓰기가 편안하게 느껴졌어요.

그렇긴 해도 Swift는 대부분 Apple 플랫폼용 앱 맥락에서 유용한데, 저는 지금 그런 걸 작성하지 않아요. 연습 문제에는 잘 맞았지만, 당장 다시 돌아올 것 같지는 않아요. 그래도 iPad에서 작성할 수 있다는 점은 정말 좋아요!

Slimline September

9월은 아주 간결하거나 작은 언어들을 탐구했어요. 저는 jq를 골랐어요. 몇 년째 애용해 온 도구죠. 그런데 저는 늘 이걸 그냥 JSON을 다루는 도구로만 생각했지, 범용 프로그래밍 언어라고는 생각하지 않았어요. 함수, 변수, 루프 등 일반적인 요소를 다 갖추고 있어서 기분 좋게 놀랐어요. 그래서 꽤 복잡한 프로그램도 작성할 수 있었죠:

# input: { "series": "1", "sliceLength": 1 }
. as {series: $series, sliceLength: $sliceLength} |
if
  $series == "" then
	"series cannot be empty" | halt_error
  elif $sliceLength > ($series | length) then
	"slice length cannot be greater than series length" | halt_error
  elif $sliceLength == 0 then
	"slice length cannot be zero" | halt_error
  elif $sliceLength < 0 then
	"slice length cannot be negative" | halt_error
  else
	.
end
| [range(0; $series | length)]
| map($series[. : . + $sliceLength])
| map(select(. | length == $sliceLength))

단순한 데이터 변환에는 한 번도 필요하지 않았던 jq 기능들을 다 써 보니 재미있었어요. 여기서는 도구가 다소 부족했지만(에디터 통합이 없는 등), jq 기능의 폭넓음에 대한 더 깊은 이해는 값진 것이었어요.

수정: Mastodon의 DJ Adams가 jq-lsp 프로젝트와 그에 해당하는 VSCode 플러그인을 알려주었어요. 이번에는 놓쳤지만, 나중에 살펴볼게요.

Object Oriented October

10월은 객체 지향 언어를 파고들었어요. 저는 머릿속으로 프로그램을 그리는 방식과 밀접하게 닮아 있는 객체 지향 설계에 정이 가요. 저는 Ruby를 골랐는데, 이상한 선택처럼 들릴 수도 있어요.

저는 세계에서 가장 큰 Ruby 코드베이스의 본거지인 Stripe에서 일해요. 그럼 확실히 "낯선" 언어로는 안 쳐주겠죠? 그 말이 다 맞지만, 우리의 Ruby 모놀리스는 "표준" Ruby와 아주 동떨어져 있어요. 모든 게 Sorbet으로 타입 검사를 하고, 코드 생성이 엄청 많으며, 모든 걸 함께 작동시키고 확장하기 위해 온갖 마법을 부려요. Stripe 안과 밖의 Ruby는 결국 같은 언어지만, 이렇게 다른 규모에서 일하는 것은 매우 다른 경험을 안겨 줘요. 그래서 저는 (Ruby를 많이 썼던 시절 이후로) 바깥의 삶이 어떤지 알고 싶었어요.

대체로 좋았어요! Ruby 자체가 훌륭하고 "프로그래머의 행복"을 주요 목표로 내세우는데, 저도 공감했어요. 한 번도 써 본 적 없는 표준 라이브러리 함수의 이름을 자주 맞힐 수 있다는 점이 좋아요. 함수형 코드를 만들기가 얼마나 쉬운지, 문법이 얼마나 편안하고 표현력이 좋은지도 좋아요.

그런데 개발자 도구가 Python에 비해 이렇게나 뒤처져 있다니 놀랐어요. 제가 너무 좋은 것에 길들여진 걸 수도 있지만, 에디터 안에서 타입 힌트를 보고 아주 빠른 린팅과 포매팅을 하는 게 제가 생각했던 것보다 더 중요해요. 전성기 때 Ruby가 그렇게 인기 있었던 언어치고는, 그런 면에서 이렇게 뒤처져 있다고 느껴져서 놀랐어요.3 함수 호출에서 괄호가 선택적이라, 함수를 인자로 넘기기가 덜 간단했던 점에도 끝내 적응하지 못했어요.

Ruby는 여전히 훌륭한 언어이고 저는 직장에서 계속 쓸 거지만, 적어도 지금으로서는 Python이 못 하는 일을 Ruby가 해 주지는 않아요.

Nibbly November

11월은 지금까지 중 가장 어려운 달이었어요. 어셈블리 언어죠. 요즘은 이걸 손으로 작성하는 게 흔치 않지만, 익숙해져 두면 유용하고 흥미로운 주제예요. 저는 현대와 미래의 웹에서 중요한 WebAssembly를 골랐어요. 보통은 컴파일 대상으로 쓰이고(직접 손으로 작성하는 게 아니라), 그래도 세상에는 이런 괴짜들을 위한 도구도 있어요.

이번 달은 예상 밖으로 준비가 잘 되어 있다고 느꼈어요. 문법은 Clojure 같았고, 언어 구조는 Zachtronics의 TIS-100 같았어요. 이상하게도 모든 연산을 맨바닥에서 시작해야 하는 게 즐거웠어요. 뭔가 옛스러운 느낌이었죠. 이런 식으로 실제로 뭔가를 해내야 한다면 싫었겠지만, 그저 재미난 진기함으로는 좋았어요. 주석을 넉넉히 달아서, 거의 읽을 만한 걸 작성할 수 있었어요:

(module
  (func (export "eggCount") (param $number i32) (result i32)
	(local $res i32) ;; result
	(local $remainder i32) ;; loop counter

	(loop $loop

  	;; $res =
  	(local.set $res
    	;; $res +
    	(i32.add
      	(local.get $res)
      	;; $number % 2
      	(i32.rem_u
        	(local.get $number)
        	(i32.const 2)
      	)
    	)
  	)

  	;; $number //= 2
  	;; (keep on stack)
  	(local.tee $number
    	(i32.div_u
      	(local.get $number)
      	(i32.const 2)
    	)
  	)

  	;; this will keep looping until remainder is 0
  	br_if $loop
	)

	local.get $res
  )
)

가장 큰 걸림돌은 문서와 자료의 부족이었어요. 어떤 전역 함수를 쓸 수 있는지조차 알아내기 어려웠어요. 그래도 이걸 실제로 쓸 일은 없으니, 일단 굴러가기 시작하자 그렇게까지 신경 쓰이지는 않았어요.

12월은 다른 범주에 맞지 않았던 언어들로 한 해를 마무리했어요. March에서 한 언어를 두 번 했기 때문에, 이번 달에는 두 개의 언어를 끝내야 했어요.

저는 Wren으로 시작했어요. 무엇보다도 Crafting Interpreters로 유명한 Bob Nystrom이 만들었죠. 세부 사항에 대한 관심, 작은 크기, 하향식 설계에 매료됐어요. 모든 게 아주 잘 고민된 것처럼 보여요. 그런 수준의 정성은 변수 스코프와 접근 제한 규칙의 세부 사항에서 드러나요. 컴파일러가 작고 주석이 잔뜩 달려 있어서, 언어 구현에 관심이 있다면 훌륭한 학습 자료예요.

Wren은 다소 다듬어지지 않은 데다 이제 거의 버려진 것 같지만, 장난감 언어치고는 괜찮다고 생각해요. 실제 서비스에 쓸 수 있는 수준을 기대하고 들어오는 사람은 없으니까요. 비실용적인 언어를 위한 자리도 세상에는 분명히 있어요.

그리고: Lua

이번 달 두 번째 선택은 Lua였어요. Wren과 대조적으로, 아주 실용적이에요. 쉽게 내장할 수 있다는 점 덕분에 Redis 스크립팅이나 Factorio 모드처럼 여러 곳에 등장해요. 객체 모델에 익숙해지는 데는 조금 걸렸지만, 금방 능률을 낼 수 있겠다는 게 보였어요. 모든 걸 해내는 구조로서의 테이블에 금방 정이 갔어요. 도구도 좋았어요. 패키지 관리자는 별 설정 없이 바로 작동했고, VSCode 확장은 주석 기반 타입 표기를 군더더기 없이 지원했어요.

당장 Lua가 필요한 일은 없지만, 널리 쓰인다는 점 때문에 도구함에 있으면 좋은 또 하나의 훌륭한 도구예요.

마무리하며

이 언어 여행은 기대했던 것보다 더 즐거웠어요. 새로운 실용 기술을 몇 가지 배웠을 뿐만 아니라, 시야가 확실히 넓어진 느낌이에요.

다음은 뭐냐고 하면, Rust를 훨씬 더 많이 배우는 것 같아요. 개발자 도구 생태계에서 Rust의 중요성은 이 시점에서 분명하고, 제가 의존하는 것들을 읽고 기여할 수 있게 되고 싶어요.

구체적으로 내놓을 결과물을 염두에 둔 건 아니지만, 읽을 Rust 책 한 권과, 어느 해에 경비로 처리한 JS 개발자를 위한 Rust 강의, 그리고 완주할 Exercism 트랙 하나가 있어요. 적어도 하나의 오픈 소스 프로젝트(아마 제가 새로 좋아하게 된 프로그램인 Just)에 기여하고 싶지만, 한 해가 어디로 데려갈지는 두고 봐야겠죠.

그때까지, 즐거운 연말 보내시고 2023년 남은 시간도 잘 보내세요!

  1. 안 쓰는 변수가 컴파일 오류라고?? 이건 좀 아니잖아요 ↩

  2. 사실 C++을 먼저 시도했어요(대학 이후로 써 본 적이 없었죠). 그냥 재미가 없어서 포기했어요 ↩

  3. 이것도 "실제" Ruby가 제가 Stripe에서 겪은 것과 다른 또 하나의 지점이에요. 그래서 두 가지를 다 겪어 볼 수 있어서 다행이에요 ↩

2024년 01월 05일 · 유용했나요?