Output & Basics
Hello, World
main :: IO ()
main = putStrLn "Hello, World!" fun main() {
println("Hello, World!")
} Haskell requires a named
main :: IO () entry point that the runtime calls once. Kotlin also needs a top-level fun main(), but requires no surrounding class or object at all — a bare top-level function is sufficient, closer to a scripting language than Java's mandatory class.Sequencing multiple outputs
main :: IO ()
main = do
putStrLn "First"
putStrLn "Second"
putStrLn "Third" fun main() {
println("First")
println("Second")
println("Third")
} Haskell needs
do-notation to sequence IO actions, since it desugars to chained monadic binds. Kotlin is strict, so statements inside main simply execute one after another in the order written — no monad or block syntax required for plain sequencing.Printing a value for inspection
main :: IO ()
main = print (Just [1, 2, 3]) fun main() {
val value: List<Int>? = listOf(1, 2, 3)
println(value)
} Haskell's
print uses the Show typeclass, deriving a textual representation for (almost) any value automatically. Kotlin's println relies on every object's inherited toString, which standard collections already implement sensibly — no extra formatting call needed, much like Haskell's derived Show.Values & Bindings
val is Haskell-style immutable binding
main :: IO ()
main =
let radius = 5.0
area = pi * radius * radius
in print area fun main() {
val radius = 5.0
val area = Math.PI * radius * radius
println(area)
} Haskell's
let ... in bindings and Kotlin's val both bind an immutable name once — val cannot be reassigned, matching Haskell's single-assignment discipline exactly, unlike Kotlin's separate var (next row).Opting in to mutation with var
import Data.IORef
main :: IO ()
main = do
ref <- newIORef 0
modifyIORef ref (+1)
value <- readIORef ref
print value fun main() {
var count = 0
count += 1
println(count)
} Haskell reaches for
IORef and threads it explicitly through IO with newIORef/modifyIORef/readIORef. Kotlin's var keyword offers a plain, in-place mutable local variable directly — no wrapper type, no monad required — and idiomatic Kotlin style, inherited from Java's imperative habits, reaches for var considerably more often than idiomatic Haskell reaches for IORef.Types & Inference
Local inference feels familiar; signatures do not
double :: Int -> Int
double x = x * 2
main :: IO ()
main = print (double 21) fun double(x: Int): Int = x * 2
fun main() {
println(double(21))
} Kotlin infers the type of local values the way Haskell does, so everyday code reads almost as unannotated as Haskell. The difference surfaces at function boundaries: Kotlin parameter types and (with rare exceptions) return types are always explicit, the one place Haskell's inference goes further than Kotlin's.
Generic functions
firstOf :: [a] -> Maybe a
firstOf [] = Nothing
firstOf (x:_) = Just x
main :: IO ()
main = print (firstOf [1, 2, 3]) fun <T> firstOf(list: List<T>): T? = list.firstOrNull()
fun main() {
println(firstOf(listOf(1, 2, 3)))
} Haskell's lowercase type variable
a and Kotlin's angle-bracketed <T> both introduce a generic type parameter, but Kotlin requires it to be written explicitly on the function — Haskell infers that firstOf is generic purely from never constraining a, where Kotlin requires the <T> annotation even though the body needs no further help.Strings
String concatenation
main :: IO ()
main = putStrLn ("Hello, " ++ "World!") fun main() {
println("Hello, " + "World!")
} Haskell's
++ is the general list-concatenation operator, since a String is really [Char]. Kotlin's + is a genuine JVM java.lang.String concatenation, since Kotlin strings are ordinary Java strings, not character lists.String interpolation
import Text.Printf (printf)
main :: IO ()
main = do
let name = "Ada"
age = 36 :: Int
printf "%s is %d\n" name age fun main() {
val name = "Ada"
val age = 36
println("${name} is ${age}")
} Haskell has no native interpolation — the idiomatic route is
Text.Printf with positional format specifiers. Kotlin has true string templates built into the language with \${ } placeholders (or bare \$name for a single identifier), closer to Python f-strings or Ruby's #{}, and needs no import.A string is a list of characters — or a real JVM class
main :: IO ()
main = print (reverse "hello") fun main() {
println("hello".reversed())
} Haskell's
String is literally a type alias for [Char], so ordinary list functions like reverse apply directly. Kotlin's String is a genuine java.lang.String, but Kotlin adds extension functions like .reversed(), .map, .filter that make it behave much like a CharSequence collection for many operations — a middle ground between Haskell's literal list and a fully opaque string type.Collections
List literals
main :: IO ()
main = print [1, 2, 3, 4, 5] fun main() {
println(listOf(1, 2, 3, 4, 5))
} Haskell's bracket literal
[1, 2, 3] becomes Kotlin's listOf(1, 2, 3) function call — Kotlin has no dedicated list-literal syntax. Under the hood the representations differ completely: a Haskell list is a lazy, singly-linked chain of cons cells, while Kotlin's List is backed by a JVM array-based structure, matching most mainstream languages rather than the functional-language linked-list convention.map and filter
main :: IO ()
main =
let doubled = map (* 2) [1, 2, 3, 4, 5]
evens = filter even doubled
in print evens fun main() {
val doubled = listOf(1, 2, 3, 4, 5).map { it * 2 }
val evens = doubled.filter { it % 2 == 0 }
println(evens)
} Both languages call these operations
map and filter. Kotlin calls them as methods on the collection using trailing-lambda syntax with the implicit parameter name it, where Haskell calls them as free functions taking the list last (map f list) — the same operation, opposite argument-order convention.Folding a collection
main :: IO ()
main = print (foldl (+) 0 [1, 2, 3, 4, 5]) fun main() {
val total = listOf(1, 2, 3, 4, 5).fold(0) { accumulator, number -> accumulator + number }
println(total)
} Haskell's
foldl and Kotlin's fold take the same pieces — initial accumulator, then a combining function — with the same argument order (accumulator first), and even reuse the same name, one of the closer standard-library vocabulary matches on the site.Key/value maps
import qualified Data.Map as Map
main :: IO ()
main =
let ages = Map.fromList [("Ada", 36), ("Alan", 41)]
in print (Map.lookup "Ada" ages) fun main() {
val ages = mapOf("Ada" to 36, "Alan" to 41)
println(ages["Ada"])
} Both languages provide an immutable map as a first-class collection: Haskell's
Data.Map (usually imported qualified) and Kotlin's mapOf, built from to-infix key-value pairs, needing no import at all. Both a missing-key Map.lookup and a missing-key Kotlin subscript return null/Nothing rather than throwing.Maybe vs. Nullable Types
Maybe becomes a nullable type, not a wrapper
findUser :: Int -> Maybe String
findUser 1 = Just "Ada"
findUser _ = Nothing
main :: IO ()
main = print (findUser 1) fun findUser(id: Int): String? = if (id == 1) "Ada" else null
fun main() {
println(findUser(1))
} Haskell's
Maybe a is a genuine wrapper type — a Just x is a different value from a bare x, and unwrapping is mandatory. Kotlin bakes nullability directly into the type system instead: String? is a type-level annotation on an ordinary String, with no wrapper allocated at all — a String or a nullable String occupy the same runtime representation, just with different compile-time guarantees, closer to Swift's Optional than to Haskell's explicit Maybe.The ?. safe-call operator
import qualified Data.Map as Map
main :: IO ()
main =
let user = Map.fromList [("name", "Ada")]
in print (fmap length (Map.lookup "name" user)) fun main() {
val user: Map<String, String> = mapOf("name" to "Ada")
println(user["name"]?.length)
} Haskell's
fmap applies a function inside a Maybe without unwrapping it — the Functor typeclass's core operation. Kotlin's ?. safe-call operator does the equivalent job syntactically: user["name"]?.length evaluates to null immediately if the lookup itself was null, otherwise calls .length on the unwrapped value — the same short-circuiting behavior, expressed as an operator rather than a typeclass method.Defaults: fromMaybe versus the Elvis operator
import Data.Maybe (fromMaybe)
main :: IO ()
main =
let maybeValue = Nothing :: Maybe Int
in print (fromMaybe 0 maybeValue) fun main() {
val maybeValue: Int? = null
println(maybeValue ?: 0)
} Haskell's
fromMaybe defaultValue maybeValue and Kotlin's maybeValue ?: defaultValue (the "Elvis operator," so named because ?: resembles a sideways emoticon) do the same job — use the value if present, fall back otherwise — but Kotlin expresses it as an infix operator rather than a named function.Platform types: a gap Haskell structurally cannot have
-- Haskell has no foreign-function boundary that silently discards
-- type information the way calling into Java from Kotlin can — every
-- value Haskell can see already has a fully known, checked type:
main :: IO ()
main = print (Just "hello") // Calling into a Java library from Kotlin, the return type is a
// "platform type" (shown as String! in tooling) — Kotlin cannot know
// whether the Java method can return null, so null-safety checking is
// silently SKIPPED for that value until it crosses into genuine
// Kotlin code with an explicit type annotation:
fun main() {
val value: String = java.lang.System.getProperty("does.not.exist")
println(value) // compiles fine, but crashes at runtime — "value"
// is actually null, and Kotlin trusted the Java
// signature instead of verifying it
} This is a real gap unique to Kotlin's JVM interop story, with no Haskell parallel at all: values returned from Java code have an unchecked "platform type," and Kotlin's null-safety guarantees silently do not apply until that value is given an explicit Kotlin type annotation, at which point Kotlin trusts the annotation rather than actually verifying it. A
NullPointerException from a mismatched platform type is exactly the class of bug Kotlin's ? system exists to prevent — except at the Java boundary, where it structurally cannot. The Kotlin side is display-only: it deliberately crashes to demonstrate the gap, so it is not run automatically.Algebraic Data Types vs. Sealed Classes
Modeling "one of several shapes"
data Shape = Circle Double | Rectangle Double Double
area :: Shape -> Double
area (Circle radius) = pi * radius * radius
area (Rectangle width height) = width * height
main :: IO ()
main = print (map area [Circle 5.0, Rectangle 3.0 4.0]) sealed class Shape {
data class Circle(val radius: Double) : Shape()
data class Rectangle(val width: Double, val height: Double) : Shape()
}
fun area(shape: Shape): Double = when (shape) {
is Shape.Circle -> Math.PI * shape.radius * shape.radius
is Shape.Rectangle -> shape.width * shape.height
}
fun main() {
val shapes = listOf(Shape.Circle(5.0), Shape.Rectangle(3.0, 4.0))
println(shapes.map(::area))
} Haskell's algebraic data types declare every variant in one
data statement; Kotlin expresses the same closed sum type as a sealed class with nested data class subtypes. sealed is the keyword doing the real work — it tells the compiler every subtype is declared in the same file (or module), which is what lets a when be checked for exhaustiveness at all, and is Shape.Circle smart-casts shape to expose .radius with no manual cast needed.Payload-free constructors: enum class, not sealed class
data Direction = North | South | East | West deriving (Show)
main :: IO ()
main = print North enum class Direction {
NORTH, SOUTH, EAST, WEST
}
fun main() {
println(Direction.NORTH)
} For payload-free variants, idiomatic Kotlin reaches for the simpler, dedicated
enum class rather than a sealed class with empty object subtypes — a genuine two-tier system: enum class for a closed set of plain constants, sealed class for a closed set of variants that need to carry different data. Haskell has one uniform data declaration that handles both cases identically.Both compilers check exhaustiveness — for closed types
-- GHC warns here (with -Wincomplete-patterns) since the
-- negative case is missing entirely:
classify :: Int -> String
classify 0 = "zero"
classify n | n > 0 = "positive"
main :: IO ()
main = putStrLn (classify 5) fun classify(n: Int): String = when {
n == 0 -> "zero"
n > 0 -> "positive"
// Kotlin's "when" used as an expression (not a statement) requires
// an "else" branch to be exhaustive for a plain Int — Kotlin can
// only prove REAL exhaustiveness for sealed classes/enums, not
// arbitrary Int ranges:
else -> "negative"
}
fun main() {
println(classify(5))
} Both compilers statically check whether a match covers every case, but the guarantee is stronger in Haskell: GHC's check applies uniformly to any data type. Kotlin's
when-as-expression exhaustiveness check only proves genuine completeness for sealed class hierarchies and enum class types — over a plain Int like this one, an else branch is always required, since Kotlin cannot enumerate every possible Int the way it can enumerate every subtype of a sealed class.Pattern Matching
case versus when
describe :: Int -> String
describe n = case n of
0 -> "zero"
n | n > 0 -> "positive"
_ -> "negative"
main :: IO ()
main = putStrLn (describe (-5)) fun describe(n: Int): String = when {
n == 0 -> "zero"
n > 0 -> "positive"
else -> "negative"
}
fun main() {
println(describe(-5))
} Haskell's
case ... of and Kotlin's when { } read as dialects of the same construct: conditions tried top to bottom with a catch-all at the end. Kotlin's no-argument when form used here evaluates each branch's condition as an independent boolean expression, closer to a chained if/else if than to Haskell's value-matching case — Kotlin's when (n) { 0 -> ...; else -> ... } subject form is the closer direct analogue for matching against a specific value.Destructuring a Pair
main :: IO ()
main =
let point = (3, 4)
(x, y) = point
in print (x + y) fun main() {
val point = Pair(3, 4)
val (x, y) = point
println(x + y)
} Haskell's built-in tuple syntax
(3, 4) and Kotlin's Pair(3, 4) class both support the same destructuring pattern, (x, y) = ..., on the left of a binding — Kotlin has no native tuple syntax beyond Pair/Triple, capping out at two and three elements respectively, where Haskell tuples support any arity.Records vs. Data Classes
Records versus a genuine data class
data Point = Point { x :: Int, y :: Int } deriving (Show, Eq)
main :: IO ()
main =
let point = Point { x = 3, y = 4 }
in print (x point, y point) data class Point(val x: Int, val y: Int)
fun main() {
val point = Point(3, 4)
println(Pair(point.x, point.y))
} Both define a named product type with labeled fields, accessed with dot syntax. Haskell needs an explicit
deriving (Show, Eq) clause to get printable, comparable records; Kotlin's data class generates toString, equals, hashCode, and a copy method automatically, with no opt-in clause required at all."Updating" a value: record syntax versus .copy()
data Point = Point { x :: Int, y :: Int } deriving (Show)
main :: IO ()
main =
let point = Point { x = 3, y = 4 }
moved = point { y = 99 }
in print moved data class Point(val x: Int, val y: Int)
fun main() {
val point = Point(3, 4)
val moved = point.copy(y = 99)
println(moved)
} Haskell's
point { y = 99 } and Kotlin's point.copy(y = 99) both produce a new value with one field replaced and every other field copied automatically — Kotlin's .copy method is generated for free on every data class, playing exactly the role Haskell's built-in record-update braces do.Functions & Currying
Anonymous functions (lambdas)
main :: IO ()
main = print (map (\x -> x * x) [1, 2, 3, 4]) fun main() {
println(listOf(1, 2, 3, 4).map { x -> x * x })
} Haskell spells a lambda
\x -> ... (the backslash evoking the Greek letter lambda). Kotlin spells the same idea { x -> ... }, and — when the parameter's name is not worth naming — can shorten it further to the implicit-parameter shorthand { it * it } seen in earlier rows.Currying: automatic in Haskell, explicit in Kotlin
add :: Int -> Int -> Int
add x y = x + y
main :: IO ()
main =
let addFive = add 5
in print (addFive 10) fun add(x: Int): (Int) -> Int = { y -> x + y }
fun main() {
val addFive = add(5)
println(addFive(10))
} In Haskell,
add 5 alone is already valid — every function is secretly a chain of one-argument functions. Kotlin functions take a fixed argument list with no automatic partial application at all; getting the same behavior means writing the currying out explicitly, a function that returns a lambda, exactly as shown — the result behaves identically to Haskell's automatic currying, but the intent must be spelled out by hand.Default arguments: a Kotlin feature with no Haskell parallel
-- Haskell has no default-argument syntax at all — every function
-- call must supply every parameter; the closest workaround is a
-- separate wrapper function or a record of options:
greet :: String -> String
greet name = "Hello, " ++ name ++ "!"
main :: IO ()
main = putStrLn (greet "Ada") fun greet(name: String, greeting: String = "Hello"): String =
"${greeting}, ${name}!"
fun main() {
println(greet("Ada")) // uses the default greeting
println(greet("Ada", "Howdy")) // overrides it
} Kotlin functions may declare a default value for any parameter, letting callers omit it entirely — a genuine convenience with no Haskell equivalent at all, since Haskell function calls always supply every argument positionally. The closest Haskell workaround is a record-of-options argument with sensible field values, considerably more ceremony than Kotlin's inline default.
Type Classes vs. Extension Functions
Type classes have no ad-hoc-polymorphism equivalent in Kotlin
class Describable a where
describe :: a -> String
data Dog = Dog
instance Describable Dog where
describe _ = "a dog"
main :: IO ()
main = putStrLn (describe Dog) class Dog
fun Dog.describe(): String = "a dog"
fun main() {
println(Dog().describe())
} Haskell resolves "which implementation for which type" through type classes, with the compiler automatically picking the right instance based on the type at the call site — genuine, nominal, type-directed dispatch. Kotlin has no type classes and no equivalent mechanism at all: an extension function like
Dog.describe() only adds a method to one specific, named type — there is no way to write one polymorphic describe that dispatches differently per type the way Haskell's does, short of falling back to an ordinary interface (weaker than Swift's protocol extensions, since Kotlin extension functions cannot be added to an interface's implementors retroactively the way a protocol conformance can).Interfaces with default methods: the closest available substitute
import Data.Char (toUpper)
class Greetable a where
greeting :: a -> String
loudGreeting :: a -> String
loudGreeting x = map toUpper (greeting x) -- default method
data Robot = Robot
instance Greetable Robot where
greeting _ = "beep boop"
main :: IO ()
main = putStrLn (loudGreeting Robot) interface Greetable {
fun greeting(): String
fun loudGreeting(): String = greeting().uppercase() // default method
}
class Robot : Greetable {
override fun greeting() = "beep boop"
}
fun main() {
println(Robot().loudGreeting())
} Kotlin interfaces, unlike Java's original interfaces, may supply a default method body directly — the closest Kotlin gets to Haskell's type-class default methods. The real difference from Haskell type classes remains: a type must explicitly declare
: Greetable at its own definition site (nominal, like Java/Swift), where a Haskell type may gain a brand-new instance of an existing class anywhere in the program, including for types the class author never anticipated.Extending types you don't own: a genuine Kotlin strength
-- Extending Int with a brand-new function in Haskell just means
-- writing an ordinary top-level function taking an Int — there is
-- no special "extension" syntax needed or possible:
isPrime :: Int -> Bool
isPrime n = n > 1 && all (\d -> n `mod` d /= 0) [2 .. n - 1]
main :: IO ()
main = print (isPrime 7) fun Int.isPrime(): Boolean =
this > 1 && (2 until this).all { this % it != 0 }
fun main() {
println(7.isPrime())
} Both languages let you add new behavior to a type you do not own (here, the built-in
Int). Haskell needs nothing special — an ordinary top-level function is already "extending" Int in every practical sense. Kotlin's extension-function syntax additionally lets the new function be called with dot-method syntax at the call site (7.isPrime() instead of isPrime(7)) — a genuine ergonomic win Haskell has no equivalent for, since Haskell has no method-call syntax at all.Either vs. Result & Exceptions
Either has no Kotlin standard-library equivalent
divide :: Int -> Int -> Either String Int
divide _ 0 = Left "divide by zero"
divide x y = Right (x `div` y)
main :: IO ()
main = print (divide 10 0) sealed class Outcome<out T> {
data class Success<out T>(val value: T) : Outcome<T>()
data class Failure(val reason: String) : Outcome<Nothing>()
}
fun divide(x: Int, y: Int): Outcome<Int> =
if (y == 0) Outcome.Failure("divide by zero") else Outcome.Success(x / y)
fun main() {
println(divide(10, 0))
} Unlike Scala's
Either (an almost exact naming match for Haskell's) or Swift's Result, Kotlin's standard library ships no general-purpose "value or typed error" type at all — the idiomatic move is to declare your own sealed class, as shown here, or reach for Kotlin's narrower built-in kotlin.Result<T> (next row), which fixes the error type to Throwable rather than letting you choose it freely the way Haskell's Either does.kotlin.Result: narrower than Either — the error type is fixed
import Control.Exception
safeDivide :: Int -> Int -> IO (Either SomeException Int)
safeDivide x y = try (evaluate (x `div` y))
main :: IO ()
main = do
result <- safeDivide 10 0
case result of
Left err -> putStrLn "Caught a division error"
Right value -> print value fun safeDivide(x: Int, y: Int): Result<Int> =
runCatching { x / y }
fun main() {
val result = safeDivide(10, 0)
result.fold(
onSuccess = { value -> println(value) },
onFailure = { println("Caught a division error") }
)
} Kotlin's built-in
Result<T> plus runCatching wraps a risky computation as a value, similar in spirit to Haskell's Control.Exception.try wrapping a risky IO action as an Either SomeException a. The key difference: Kotlin's Result's failure case is always a Throwable — you cannot choose your own error type the way Haskell's Either lets you choose String, a custom error enum, or anything else.Laziness vs. Strictness
Infinite lists need Sequence — not the default List
main :: IO ()
main = print (take 5 [1..]) fun main() {
val numbers = generateSequence(1) { it + 1 }
println(numbers.take(5).toList())
} Haskell's
[1..] is an ordinary list that only works because Haskell is lazy by default, generating each element on demand as take asks for it. Kotlin's default List is strict and would try to build the entire infinite list immediately (and hang forever); Kotlin's separate Sequence type (created here with generateSequence) is the one that plays Haskell's "infinite list" role, evaluating elements lazily, on demand.lazy {}: opt-in, not the default
main :: IO ()
main = do
let expensive = 2 ^ 20 -- not computed until actually demanded
putStrLn "before use"
print expensive fun main() {
val expensive by lazy {
println("computing now")
Math.pow(2.0, 20.0)
}
println("before use")
println(expensive) // "computing now" prints here, on first access
} In Haskell, every binding is lazy unless forced otherwise —
expensive is not actually computed until print demands it. Kotlin is strict by default, so achieving the same deferred, compute-once-and-cache behavior requires the explicit by lazy { } property delegate — laziness is opt-in machinery in Kotlin, not the ambient evaluation model, though lazy does genuinely cache its result after the first access, matching Haskell's thunk-memoization behavior once triggered.Gotchas for Haskell Developers
Kotlin is not purely functional — mutation and OOP are always available
-- Haskell structurally cannot mutate a value in place outside of
-- an explicit IORef/STRef/MVar — there is no escape hatch:
main :: IO ()
main = print [1, 2, 3] fun main() {
// Kotlin's standard library ships genuinely mutable collections
// right alongside the immutable ones used everywhere else on
// this page — nothing stops reaching for them:
val numbers = mutableListOf(1, 2, 3)
numbers.add(4)
println(numbers)
} Haskell has no escape hatch into ordinary in-place mutation outside of explicit
IORef/STRef/MVar machinery threaded through IO. Kotlin, being a hybrid OOP/FP language built for the JVM and Java interop, ships genuinely mutable collections (mutableListOf, MutableMap, etc.) as first-class citizens right alongside the immutable ones — nothing in the language stops a Kotlin codebase from being written in an entirely imperative, Java-like style, a discipline Haskell enforces structurally that Kotlin only encourages by convention.The !! not-null assertion is an escape hatch back to Haskell-style crashes
main :: IO ()
main = print (Nothing :: Maybe Int) fun main() {
val maybeValue: Int? = null
// The line below would crash at runtime with a
// "NullPointerException" — Kotlin's "!!" forces treating a
// nullable as non-null without checking, an explicit escape
// hatch back to the kind of partial-function crash Haskell's
// "fromJust" also allows:
// println(maybeValue!!)
println(maybeValue)
} Kotlin's type system is meant to eliminate unchecked null access, but the
!! not-null assertion operator is a deliberate, always-available escape hatch that throws immediately if the value is actually null — functionally identical to Haskell's fromJust, which is exactly as unsafe and exactly as tempting to reach for. Idiomatic style in both languages treats this operator/function as a last resort, not a first instinct, and linters in both ecosystems commonly flag its use.