강한 타입과 함수형 패러다임, 그리고 dataHaskell/dataframe 생태계를 통해 하스켈이 데이터 사이언스에 적합한 이유를 예제와 함께 살펴본다.
URL: https://jcarroll.com.au/2025/12/05/haskell-is-a-great-language-for-data-science/
저는 몇 년째 하스켈을 배우고 있는데, 강한 타입과 함수형 접근을 비롯해 많은 기능이 정말 마음에 듭니다. R에서 그리웠던 몇 가지가 부족하다고 생각했지만, dataHaskell 프로젝트를 발견한 뒤 생각이 바뀌었습니다.
최근에는 R에 강한 타입을 더하려는 시도들이 몇 가지 있었습니다. 예를 들어 vapour, typr, {rlang}의 체크를 활용하는 방법 등이 있고, 아예 코어 레벨에서의 구현에 대한 논의도 있었습니다. 예컨대 2025년 9월에 이어 2025년 11월에도 논의가 계속됐습니다. 이런 시도들은 R을 타입 쪽으로 ‘구부리려’ 하지만, 어쩌면 아예 처음부터 타입이 있는 해법이 더 말이 될지도 모릅니다.
이 글에서는 몇 가지 기능을 보여주고, 왜 하스켈이 데이터 사이언스 언어로 좋다고(훌륭하다고?) 생각하는지 설명해 보겠습니다.
저는 예전에 하스켈에 대해 열 번도 넘게 글을 올렸지만, 실제 업무에서의 장점보다는 장난감 문제를 더 많이 다뤘습니다(예: 작년에 Advent of Code를 하스켈로 많이 풀었습니다). 하스켈을 더 적극적으로 사용하려고 노력 중이고, 커스텀 {knitr} 엔진을 작동시키는 데도 성공했습니다. 아래가 ````{haskell}` 블록이 동작하게 만드는 비장의 소스입니다.
rknitr::knit_engines$set(haskell = function(options) { code <- options$code codefile <- tempfile(fileext = ".hs") codefile_brace <- tempfile(fileext = ".hs") on.exit(file.remove(codefile, codefile_brace)) writeLines(c(":script dataframe", "", code), con = codefile) system2('hscript', codefile, stdout = codefile_brace) out <- system2( file.path(path.expand('~'), '.ghcup/bin/ghc'), c('-e',"':script ", codefile_brace, "'"), stdout = TRUE ) knitr::engine_output(options, code, out) })
이 코드는 코드 줄들을 임시 파일에 쓰되, 앞에 몇 가지 설정 옵션을 붙입니다. 그런 다음 사실상 ghc -e ':script file.txt'를 실행하고 임시 파일을 삭제합니다. 더 깔끔한 코드 블록을 위해, 코드는 중간에 awk 스크립트를 거치는데, 이는 여러 줄로 된 구문 주변에 :{ 블록을 삽입해 Jupyter 노트북에서 보이는 모양을 재현하는 데 도움을 줍니다. 결과는 코드 블록에 그대로 표시되므로, 이는 “라이브” 출력입니다.
map (+5) [2..8]
text## [7,8,9,10,11,12,13]
멋지죠?
각 코드 블록을 독립적인 스크립트로 취급하기 때문에 블록 간에 약간 반복이 생깁니다. 필요하면 적절히 echo 옵션으로 숨기겠지만, 그 외에는 각 블록이 올바른 전처리만 거치면 ‘스크립트’로 실행 가능해야 합니다.
R이나 파이썬만 보다가 하스켈을 보면 조금 다르긴 하지만, 무슨 일이 벌어지는지 이해하는 데 큰 노력이 들지는 않습니다. 우선 R에서는 함수 호출에 괄호를 쓰지만, 하스켈에서는 공백을 씁니다. 즉 sum(x) 대신 sum x라고 씁니다. 괄호는 여전히 여러 요소를 묶어 함께 평가해야 할 때 그룹핑 용도로 사용합니다.
리스트는 기본 데이터 타입이며 대괄호로 표기합니다. 예: [3,4,5]. 그리고 리스트는 단 하나의 타입만 담아야 합니다. 강타입 언어라면 놀랄 일도 아니죠. 단일 숫자는 Double 타입일 수 있고, 그 리스트는 [Double] 타입이 됩니다.
파이프 기반 워크플로에 너무 익숙해졌다고 걱정한다면 안심하세요. dataHaskell의 dataframe 패키지는 익숙한 파이프 연산자를 추가해 줍니다.
haskell[2,8,7,10,1,9,5,3,4,6] |> reverse |> take 5
text## [6,4,3,5,9]
중요한 차이는, 왼쪽 값을 오른쪽 함수의 _마지막 인자_로 전달한다는 점입니다(첫 번째 인자로 넘기지 않습니다). 하스켈 함수들이 보통 어떻게 작성되는지를 생각하면 이 방식이 흐름이 더 자연스럽습니다. 예를 들어,
haskelltake 3 [1,2,3,4,5,6] -- vs [1,2,3,4,5,6] |> take 3
text## [1,2,3] ## [1,2,3]
가운데 줄이 보여주듯, 주석은 하이픈 두 개 --로 시작합니다. 여러 줄 주석은 {-와 -} 사이에 씁니다.
함수를 작성해야 한다면(보통 camelCase 사용), 타입 정의를 붙일 수 있습니다. 컴파일러가 대부분 추론해 주긴 하지만(그리고 가독성에도 도움이 됩니다)요. 구현 위에 한 줄을 추가해 적습니다. 타입이 제네릭이라면 특정 타입 대신 a 같은 플레이스홀더를 쓸 수 있습니다. 기술적으로 모든 함수는 인자를 하나만 받는데, 다른 함수를 반환할 수도 있습니다(커링 참고). 다만 시그니처에서는 더 명시적으로 보입니다. 예: [a] -> a -> [a]는 리스트와 값을 받아 리스트를 반환하는 함수를 의미합니다.
haskellappendValueToList :: [a] -> a -> [a] appendValueToList xs y = xs ++ [y] appendValueToList [2,4,6] 8 appendValueToList ["f", "o", "o"] "t"
text## [2,4,6,8] ## ["f","o","o","t"]
마침표는 함수 합성에 사용합니다. 즉,
haskellimport Data.List (sort) (reverse . sort) [2,8,7,10,1,9,5,3,4,6]
text## [10,9,8,7,6,5,4,3,2,1]
리스트에 ‘정렬 후 뒤집기’라는 합성 연산을 적용합니다. import가 있는 이유는 ‘기본’ 라이브러리(“Prelude”)에 sort 함수가 없어서 가져와야 하기 때문입니다.
사실 아래에서 보여주는 코드를 쓰려면 몇 가지를 더 import해야 하지만, 앞서 엔진 정의의 :script dataframe 줄을 통해 코드 블록에 삽입됩니다. 이는 실행 파일을 호출해 코드 블록을 전체 프로그램의 main 함수 안에 들어있는 것처럼 실행합니다. 그 덕분에 파일을 읽고 결과를 출력하는 것 같은 IO 연산을 인라인으로 사용할 수 있습니다. 이런 ‘스크립팅’ 컨텍스트가 없으면 조금 더 까다로워지지만, 저는 데이터 사이언스를 할 때 이런 스크립팅 컨텍스트가 _잘 작동한다_는 점을 보여주려는 것입니다.
그렇다면, 이걸 어디에 쓰냐고요?
Claus Wilke가 파이썬이 데이터 사이언스에 훌륭한 언어가 아니라는 내용의 이 (후속) 글을 올렸는데, 거기서 말한 요지에는 동의합니다. 다만 그중 일부는 개인 취향도 있다고 봅니다. 저는 “익숙한 도구를 쓰자”는 입장이고, 수천 명의 데이터 사이언티스트가 파이썬으로 데이터 사이언스를 잘 하고 있다는 사실을 부정할 수는 없습니다.
하지만 “좋은 데이터 사이언스 언어란 무엇인가”라는 질문이 저를 멈춰 세웠고, 곰곰이 생각해 보니 하스켈이(적어도 dataHaskell 생태계와 dataframe 패키지와 함께라면) 그 조건들을 충족한다는 결론에 이르렀습니다. 아래 내용은 파이썬을 공격하거나 R을 불평하려는 것이 아니라, “그게 좋다면 이것도 한번 봐!”라는 스타일로 받아들여 주세요.
요즘은 많은 언어가 데이터프레임 비슷한 걸 갖고 있습니다—고마워요 R!—예를 들어 파이썬에는 Pandas/Polars, Julia에는 DataFrames.jl, Kotlin에도 DataFrame이 있습니다. 하스켈도 dataframe로 가능합니다. 저는 최근에 이걸 배우고 있습니다.
Claus의 글에서, 파이썬보다 R이 데이터 사이언스에 더 나은 언어가 되는 특징은(의역하면) 다음과 같았습니다.
각 항목을 하스켈이 어떻게 다루는지 보겠습니다.
Claus는 파이썬의 call-by-reference 의미론이 함수 간 스코프를 통해 변수를 의도치 않게 수정할 수 있다고 설명합니다. 하스켈에는 확실히 이런 문제가 없습니다. 모든 것은 불변(immutable)이고 함수는 “순수(pure)”합니다(부작용이 없지만, 타입이 붙은 부작용 ‘명령’으로 상호작용은 할 수 있습니다). 데이터 객체에 뭔가를 “한다”면, 함수를 통해 입력으로 넘기고 새로운 객체를 출력으로 받습니다. 실수로 변수를 바꿀 위험이 없지만, 반대로 함수 없이는 직접 바꿀 수 없다는 단점이 있죠.
R에서는 다음이 매우 간단합니다.
ra <- c(2, 9, 6) a[2] <- 4 a
text## [1] 2 4 6
하스켈에서는 이런 식의 변경은 금지입니다. 리스트에서 값을 뽑아오는 연산자(0부터 시작)를 사용할 수는 있습니다.
haskella = [2,9,6] a !! 1
text## 9
하지만 두 번째 원소에 다른 값을 “대입”할 방법은 없습니다. 대신 벡터를 분해한 뒤 새 값을 끼워 넣어 다시 조립해야 합니다.
haskella = [2,9,6] updateSecond :: [a] -> a -> [a] updateSecond (x:_:z) y = x : y : z updateSecond xs _ = xs updateSecond a 4
text## [2,4,6]
실수로 저런 코드를 ‘우연히’ 작성할 일은 없겠죠.
여기서는 타입 정의도 포함했는데, 이는 “어떤 타입 a의 리스트([a])와 타입 a의 단일 값을 받아 같은 타입의 리스트([a])를 반환하는 함수”라고 읽으면 됩니다. 참고로 이 예시는 ghci에서 인터랙티브로 실행할 때는 정의를 :{와 :}로 감싸야 할 수도 있는데, 여기서는 앞에서 말한 전처리 트릭을 쓰고 있습니다.
진짜로 불변 데이터를 갖는다는 점에서 한 표: 값을 “바꾸는” 유일한 방법은 함수로 처리하고 재바인딩하는 것뿐입니다.
이 부분은 하스켈이 특히 빛납니다. R에서 어떤 값이 없을 수도 있다면 NA를 사용합니다(실제로는 원하는 flavour/class에 따라 NA_character_ 같은 약식 표기). 이런 값을 수학 계산에 넣으면 ‘오염(poison)’되어 NA가 반환됩니다. 예:
sum(1, NA, 3)
text## [1] NA
이를 피하려고 대부분의 함수는 na.rm 인자를 제공해, 계산 전에 결측을 제거하도록 지시합니다.
sum(1, NA, 3, na.rm = TRUE)
text## [1] 4
여기서 일어나는 일은, R이 “결측일 수도 있는 값”을 인코딩한다는 점입니다. 하스켈은 이를 Data.Maybe 패키지로 형식화했고, 결측(Nothing)인지 확실히 값이 있는지(Just x)를 명시적으로 다뤄야 합니다.
haskellnon_missing = [1, 2, 3, 4] has_missing = [Just 1,Just 2,Nothing,Just 4] :t non_missing :t has_missing
text## non_missing :: Num a => [a] ## has_missing :: Num a => [Maybe a]
여기서 has_missing이 Maybe 타입임을 볼 수 있습니다.
sum non_missing
text## 10
후자는 그냥 sum할 수 없습니다. Maybe Integer를 합산하는 함수가 없어서 오류가 납니다.
sum has_missing
texts:7:1: error: [GHC-39999] • No instance for ‘Num (Maybe Integer)’ arising from a use of ‘it’ • In the first argument of ‘print’, namely ‘it’ In a stmt of an interactive GHCi command: print it | 7 | sum has_missing | ^^^^^^^^^^^^^^^
먼저 Nothing을 제거한 다음, 보통은 Maybe 컨텍스트에서 값을 ‘언랩’해야 합니다.
haskellimport Data.Maybe sum $ map fromJust $ filter isJust has_missing
text## 7
또는 이렇게도 됩니다.
sum (catMaybes has_missing)
text## 7
혹은 좀 더 멋있게:
sum [x | Just x <- has_missing]
text## 7
핵심은, 결측이 존재한다면 반드시 그 결측을 _처리해야 한다_는 점입니다. 또한 이것은 Double 컬럼이라면 _결측값이 없다_는 뜻이기도 합니다(Maybe Double이 아니라면). 따라서 안전하게 합계를 낼 수 있고, 컴파일러가 결측이 없다는 것을 알고 얻는 다양한 성능 이점도 따라옵니다.
Claus의 예시라면, 이 계산의 끝에서 제대로 된 Nothing을 만들 수 있습니다.
fmap (fmap (> 3)) x
text## [Just False,Just False,Nothing,Just True,Just True]
제대로 된 결측값이라는 점에서 한 표.
하스켈은 배열 언어가 아니므로, 벡터화가 내장되어 있지 않은 건 맞습니다. 하지만 Claus 글의 끝부분에서 R의 한계를 언급하며 “R에는 스칼라 데이터 타입이 없다”고 인정한 점은 짚고 넘어갈 만합니다. 하스켈에는 스칼라, 벡터, 배열이 모두 있고, 이를 순회하려면 무엇을 반복할지 명확히 해야 합니다. 변수의 “타입”에는 차원성도 포함되므로 Double은 [Double](Double의 리스트)와 다릅니다.
하스켈은 함수형 언어라 원하는 모든 종류의 map이 있습니다. 모나드/어플리커티브 전용도 포함해서요. 따라서 반복하려면 map을 써야 하지만, 값이 하나 이상이었다는 사실에 놀랄 일도 없습니다.
게다가 컴파일 언어이기 때문에 컴파일러가 다양한 벡터 연산 최적화를 수행할 수 있습니다. 예를 들어 “fusion”을 사용하면 filter와 map을 합쳐 중간 벡터를 아예 만들지 않게 할 수 있습니다.
이는 예를 들어
foldr (+) 0 . map (*2) . filter even
같은 함수 스택이, 순진하게 구현하면 짝수를 거르는 데 한 번 전체 패스, 거른 값들을 두 배 하는 데 반 패스, 더하는 데 또 반 패스가 필요하겠지만, 실제로는 한 번의 패스로 끝낼 수 있음을 뜻합니다.
또한 치환이 참이라는 확신이 있다면 rewrite rules를 추가할 수도 있습니다(많은 라이브러리가 조건을 보장할 수 있어 이런 룰을 구현합니다). 유한 리스트를 두 번 뒤집는 건 항등 연산이므로 시간이 0이 되어야 하니 다음을 추가할 수 있습니다.
haskell{-# RULES "reverse.reverse/id" reverse . reverse = id #-}
그러면 이중 reverse는 항등 함수로 대체됩니다.
이런 룰이 없더라도 하스켈은(컴파일 언어이므로) 빠릅니다.
haskellx = [1..1000000000] :set +s a = reverse $ reverse x
(0.00 secs, 0 bytes)
업데이트: Alice가 왜 여기서 이렇게 빠르게 보이는지 친절히 설명해 주었습니다. 하스켈은 지연 평가(lazy)라서, 제가 저 코드에서 사실상 아무것도 _평가_하지 않았던 겁니다. 결과의 길이를 계산하려고 하면 시간이 걸립니다. 전적으로 제 실수이고, 이 비교는 공정하지 않습니다.
R에서 인라인으로 실행하기에는 꽤 실망스럽습니다.
rx <- seq_len(1e9) system.time(rev(rev(x)))
textuser system elapsed 4.596 2.543 8.824
이건 단순히 컴파일의 문제가 아닙니다. R도 함수에 대해 JIT 컴파일이 있지만, 하스켈이 쓰는 컴파일러 트릭이 부족해서 컴파일된 버전도 크게 낫지 않습니다.
rrevrev <- function(x) { rev(rev(x)) } revrev_comp <- compiler::cmpfun(revrev) system.time(revrev_comp(x))
textuser system elapsed 4.035 0.739 4.777
즉, 벡터화는 없지만, 이를 보완할 만큼의 컴파일러 트릭이 있을지도 모릅니다—한 표.
여기서부터가 진짜 재미있습니다. dataHaskell 생태계의 dataframe 패키지는 익숙한 슬라이싱/다이싱을 제공합니다. 데이터프레임의 일반적인 확인부터 시작해 봅시다.
haskelldf <- D.readParquet "iris.parquet" D.describeColumns df
text## --------------------------------------------------------- ## Column Name | # Non-null Values | # Null Values | Type ## -------------|-------------------|---------------|------- ## Text | Int | Int | Text ## -------------|-------------------|---------------|------- ## variety | 150 | 0 | Text ## petal.width | 150 | 0 | Double ## petal.length | 150 | 0 | Double ## sepal.width | 150 | 0 | Double ## sepal.length | 150 | 0 | Double
(저 <-에 속지 마세요—하스켈에서 CPU 바깥, 예컨대 디스크에서 파일을 읽는 등의 작업을 할 때 쓰는 방식입니다.) D.dimensions로 전체 형태를 얻을 수 있고, D.nRows, D.nColumns 같은 더 구체적인 헬퍼도 있어 텍스트 출력 등에 포함시킬 수 있습니다.
haskellimport Text.Printf (printf) df <- D.readParquet "iris.parquet" D.dimensions df printf "%d rows, %d columns" (D.nRows df) (D.nColumns df)
text## (150,5) ## 150 rows, 5 columns
dplyr-스러운 연산들이 많이 제공되고, 강타입 구조와의 상호작용을 어떻게 할지에 대해 많은 고민이 녹아 있습니다.
haskelliris <- D.readParquet "iris.parquet" iris |> D.filterWhere (F.col @Text "variety" .== "Setosa") |> D.filterWhere (F.col @Double "sepal.length" .> 5.4)
text## ----------------------------------------------------------------- ## sepal.length | sepal.width | petal.length | petal.width | variety ## -------------|-------------|--------------|-------------|-------- ## Double | Double | Double | Double | Text ## -------------|-------------|--------------|-------------|-------- ## 5.8 | 4.0 | 1.2 | 0.2 | Setosa ## 5.7 | 4.4 | 1.5 | 0.4 | Setosa ## 5.7 | 3.8 | 1.7 | 0.3 | Setosa ## 5.5 | 4.2 | 1.4 | 0.2 | Setosa ## 5.5 | 3.5 | 1.3 | 0.2 | Setosa
하지만 dataframe은 템플릿 하스켈을 통해 한 걸음 더 나아갑니다. (다소 넓은 스코프이긴 하지만) 컬럼을 변수로 노출시킬 수 있어서 다음이 동작합니다.
haskelliris <- D.readParquet "iris.parquet" -- 컬럼을 표현식으로 사용할 수 있게 만들기 :exposeColumns iris iris |> D.derive "sepal.ratio" (sepal_width / sepal_length) |> D.take 5
text## sepal_length :: Expr Double ## sepal_width :: Expr Double ## petal_length :: Expr Double ## petal_width :: Expr Double ## variety :: Expr Text ## -------------------------------------------------------------------------------------- ## sepal.length | sepal.width | petal.length | petal.width | variety | sepal.ratio ## -------------|-------------|--------------|-------------|---------|------------------- ## Double | Double | Double | Double | Text | Double ## -------------|-------------|--------------|-------------|---------|------------------- ## 5.1 | 3.5 | 1.4 | 0.2 | Setosa | 0.6862745098039216 ## 4.9 | 3.0 | 1.4 | 0.2 | Setosa | 0.6122448979591836 ## 4.7 | 3.2 | 1.3 | 0.2 | Setosa | 0.6808510638297872 ## 4.6 | 3.1 | 1.5 | 0.2 | Setosa | 0.673913043478261 ## 5.0 | 3.6 | 1.4 | 0.2 | Setosa | 0.72
결과 앞에 출력된 정보는 노출된 컬럼에 대한 것입니다. 그리고 점/마침표가 언더스코어로 바뀐 점도 중요합니다. 하스켈에서 마침표는 위에서 설명한 것처럼 합성에 쓰이기 때문입니다(위 참고).
여러 동사가 지원되므로 더 상세한 변환도 할 수 있습니다.
haskelliris <- D.readParquet "iris.parquet" :exposeColumns iris iris |> D.filterWhere ( sepal_width .> 2.6 ) |> D.groupBy ["variety"] |> D.aggregate [ "n" .= F.count petal_length , "sl_mean" .= F.mean sepal_length , "pl_mean" .= F.mean petal_length ]
text## sepal_length :: Expr Double ## sepal_width :: Expr Double ## petal_length :: Expr Double ## petal_width :: Expr Double ## variety :: Expr Text ## --------------------------------------------------------- ## variety | n | sl_mean | pl_mean ## -----------|-----|-------------------|------------------- ## Text | Int | Double | Double ## -----------|-----|-------------------|------------------- ## Versicolor | 34 | 6.099999999999998 | 4.435294117647058 ## Setosa | 49 | 5.016326530612244 | 1.4653061224489798 ## Virginica | 43 | 6.651162790697675 | 5.57674418604651
그리고 기억하세요. Double 컬럼에는 결측이 없다는 것을 알고 있기 때문에(Maybe Double이 아니라면), 평균을 낼 때 na.rm 같은 복잡한 문제를 걱정할 필요가 없습니다.
NSE는 모든 곳에서 완벽히 작동하는 건 아니지만, 때로는 문자열로도 충분합니다. 예:
haskelliris <- D.readParquet "iris.parquet" D.plotScatter "sepal.length" "sepal.width" iris
text## 4.5│ ## │ ⠈ ## │ ⠠ ## │ ⠂ ## │ ⡀ ⠁ ## │ ⠠ ⠠ ⠄ ⠄ ## │ ⠐ ⠐ ⠂ ## │ ⠁ ⠈ ⡁⢀ ⡀ ⢀ ⠈ ## │ ⠄ ⠄ ⠄⠠ ⠄ ⠄ ⠄ ⠠ ⠄ ## │ ⡀ ⡀⢀ ⡂⠐ ⢀ ⠂⢀ ⡀ ⠂⢀ ⡀⢀ ⢀ ## 3.2│ ⠄ ⠄⠠ ⠠ ⠄ ⠄ ## │ ⠐ ⠂ ⠂⠐ ⠂ ⠂ ⠂⠐ ⠐ ⠂⠐ ⠂⠐ ⠂⠐ ⠂⠐ ⠐ ⠂ ## │ ⠁ ⡁⢈ ⡀ ⠁⢈ ⢈ ⡁⢈ ⡀⠈ ⢀ ⠁⢀ ⡀ ## │ ⠄ ⠄ ⠄ ⠄ ⠄⠠ ## │ ⠐ ⠐ ⠂ ⠐ ⠂ ## │ ⢈ ⠈ ⢈ ⠁⠈ ⠁ ⠁ ## │ ⠠ ⠄ ⠠ ⠄ ## │ ⠂ ⠐ ## │ ⡀ ## 1.9│ ## └──────────────────────────────────────────────────────────── ## 4.1 6.1 8.1 ## ## ⣿ sepal.length vs sepal.width
(코드 블록에서 ANSI 시퀀스가 동작하도록 https://blog.djnavarro.net/posts/2021-04-18_pretty-little-clis/를 따라했습니다.)
dplyr와 더 자세한 비교는 dataframe 문서에 나와 있습니다.
그러니까, NSE? 한 표!
강타입 언어의 힘, 그리고 데이터 사이언스에 초점을 맞춘 패키지가 R(또는 파이썬) 사용자가 기대하는 기능을 제공할 수 있음을 보여드렸길 바랍니다. 저는 하스켈(과 dataHaskell 생태계)이, 강타입 언어에서 데이터 사이언스를 하고 싶고, 매우 똑똑한 컴파일러로부터 큰 성능 향상을 얻고 싶은 우리에게 충분히 실용적인 선택지가 될 수 있기를 기대합니다.
dataHaskell이 궁금하다면 이 글을 확인해 보시고 한번 써 보세요. 우리는 devcontainer와 호스팅된 노트북 솔루션을 통해 시작 장벽을 낮추는 작업을 하고 있으며, 더 많은 데이터 사이언티스트로부터 생태계가 무엇을 지원하면 좋을지 의견을 듣고 싶습니다.
저는 하스켈이 데이터 사이언스에 정말 훌륭한 언어라고 믿습니다!
언제나 그렇듯, 저는 Mastodon에서도 만나실 수 있고 아래 댓글 섹션도 열려 있습니다.
devtools::session_info()
text## ─ Session info ─────────────────────────────────────────────────────────────── ## setting value ## version R version 4.4.1 (2024-06-14) ## os macOS 15.6.1 ## system aarch64, darwin20 ## ui X11 ## language (EN) ## collate en_US.UTF-8 ## ctype en_US.UTF-8 ## tz Australia/Adelaide ## date 2025-12-16 ## pandoc 3.8.2.1 @ /opt/homebrew/bin/ (via rmarkdown) ## quarto 1.7.31 @ /usr/local/bin/quarto ## ## ─ Packages ─────────────────────────────────────────────────────────────────── ## package * version date (UTC) lib source ## blogdown 1.21.1 2025-06-28 [1] Github (rstudio/blogdown@33313a5) ## bookdown 0.41 2024-10-16 [1] CRAN (R 4.4.1) ## bslib 0.9.0 2025-01-30 [1] CRAN (R 4.4.1) ## cachem 1.1.0 2024-05-16 [1] CRAN (R 4.4.0) ## cli 3.6.5 2025-04-23 [1] CRAN (R 4.4.1) ## devtools 2.4.6 2025-10-03 [1] CRAN (R 4.4.1) ## digest 0.6.38 2025-11-12 [1] CRAN (R 4.4.1) ## ellipsis 0.3.2 2021-04-29 [1] CRAN (R 4.4.0) ## evaluate 1.0.5 2025-08-27 [1] CRAN (R 4.4.1) ## fansi 1.0.7 2025-11-19 [1] CRAN (R 4.4.3) ## fastmap 1.2.0 2024-05-15 [1] CRAN (R 4.4.0) ## fs 1.6.6 2025-04-12 [1] CRAN (R 4.4.1) ## glue 1.8.0 2024-09-30 [1] CRAN (R 4.4.1) ## htmltools 0.5.8.1 2024-04-04 [1] CRAN (R 4.4.0) ## jquerylib 0.1.4 2021-04-26 [1] CRAN (R 4.4.0) ## jsonlite 2.0.0 2025-03-27 [1] CRAN (R 4.4.1) ## knitr 1.50 2025-03-16 [1] CRAN (R 4.4.1) ## lifecycle 1.0.4 2023-11-07 [1] CRAN (R 4.4.0) ## magrittr 2.0.4 2025-09-12 [1] CRAN (R 4.4.1) ## memoise 2.0.1 2021-11-26 [1] CRAN (R 4.4.0) ## pkgbuild 1.4.8 2025-05-26 [1] CRAN (R 4.4.1) ## pkgload 1.4.1 2025-09-23 [1] CRAN (R 4.4.1) ## purrr 1.2.0 2025-11-04 [1] CRAN (R 4.4.1) ## R6 2.6.1 2025-02-15 [1] CRAN (R 4.4.1) ## remotes 2.5.0 2024-03-17 [1] CRAN (R 4.4.1) ## rlang 1.1.6 2025-04-11 [1] CRAN (R 4.4.1) ## rmarkdown 2.30 2025-09-28 [1] CRAN (R 4.4.1) ## rstudioapi 0.17.1 2024-10-22 [1] CRAN (R 4.4.1) ## sass 0.4.10 2025-04-11 [1] CRAN (R 4.4.1) ## sessioninfo 1.2.3 2025-02-05 [1] CRAN (R 4.4.1) ## usethis 3.2.1 2025-09-06 [1] CRAN (R 4.4.1) ## vctrs 0.6.5 2023-12-01 [1] CRAN (R 4.4.0) ## xfun 0.54 2025-10-30 [1] CRAN (R 4.4.1) ## yaml 2.3.10 2024-07-26 [1] CRAN (R 4.4.0) ## ## [1] /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/library ## ## ──────────────────────────────────────────────────────────────────────────────