One of the things I really miss from C# when using Haskell is the dot-syntax for properties and methods. I like dot-syntax for methods for a few reasons:
- Explorability of the language. You type
xyz.
and the IDE shows you what operations are available onxyz
via IntelliSense. - Namespace management
- You can have a method
move
on one class and another (unrelated) methodmove
on another class and there's no name clash. - Haskell folks will point to type classes for this sort of thing. However, that only makes sense if the
move
is semantically the same for both classes. - When all you have is Haskell modules for namespace management, you end up in
import qualified
land which gets messy fast.
- You can have a method
Well, the good news is, GHC is getting dot-syntax for records.
So, can we abuse this new feature to get something like dot-syntax for methods?
Let's define a Point
record which has a distance
"method" on it:
data Point = Point { x :: Double, y :: Double, distance :: Point -> Double }
And a way to show it:
instance Show Point where
show p = "Point { x = " ++ show p.x ++ ", y = " ++ show p.y ++ " }"
Now let's define a function to get the distance between two points:
point_distance :: Point -> Point -> Double
point_distance a b = sqrt ((b.x - a.x)^2 + (b.y - a.y)^2)
And finally a constructor for points which takes care of setting up the distance
method:
make_point x y =
let
base = Point x y (\p -> 0.0)
in
Point base.x base.y (\other -> point_distance base other)
OK, now to see it in action. Let's define three points:
a = make_point 0.0 0.0
b = make_point 1.0 1.0
c = make_point 3.0 3.0
Find the distance betweeen a
and b
:
ab = a.distance b
Similar for a
and c
:
ac = a.distance c
And b
and c
:
bc = b.distance c
The new RecordDotSyntax
feature is available in GHC 9.2.1-alpha2.
See the file point.hs for the full example shown above. It can be loaded into ghc-9.2.1-alpha2 via:
$ ghci point.hs
Feel free to comment in the issues if you have suggestions for other approaches to implementing this sort of thing.
See this r/haskell thread for some discussion.
User friedbrice made some suggestions in this thread.
Here's an implementation based on his suggestions which implements methods for distance
, length
, div_n
, and norm
:
{-# LANGUAGE OverloadedRecordDot, OverloadedRecordUpdate, DuplicateRecordFields #-}
{-# LANGUAGE RecordWildCards #-}
data Point = Point {
x :: Double,
y :: Double,
distance :: Point -> Double,
length :: Double,
div_n :: Double -> Point,
norm :: Point
}
instance Show Point where
show p = "Point { x = " ++ show p.x ++ ", y = " ++ show p.y ++ " }"
make_point :: Double -> Double -> Point
make_point x y =
let
distance b = sqrt ((b.x - x)^2 + (b.y - y)^2)
length = sqrt (x^2 + y^2)
div_n n = make_point (x/n) (y/n)
norm = div_n length
this = Point {..}
in
this
-- Example expressions:
a = make_point 0.0 0.0
b = make_point 1.0 1.0
c = make_point 3.0 3.0
ab = a.distance b
ac = a.distance c
bc = b.distance c
result_distance = (make_point 1.0 1.0).distance (make_point 10.0 10.0)
result_length = b.length
result_div_n = c.div_n c.length
result_norm = c.norm
User Dark_Ethereal suggested a completely different approach based on HasField
.
Here's an implementation based on his suggestions:
{-# LANGUAGE OverloadedRecordDot, OverloadedRecordUpdate, DuplicateRecordFields #-}
{-# LANGUAGE DataKinds #-}
import GHC.Records
data Point = Point { x :: Double, y :: Double }
instance Show Point where
show p = "Point { x = " ++ show p.x ++ ", y = " ++ show p.y ++ " }"
instance HasField "distance" Point (Point -> Double) where
getField a b = sqrt ((b.x - a.x)^2 + (b.y - a.y)^2)
instance HasField "length" Point (Double) where
getField a = sqrt (a.x^2 + a.y^2)
instance HasField "div_n" Point (Double -> Point) where
getField a n = Point (a.x / n) (a.y / n)
instance HasField "norm" Point (Point) where
getField a = a.div_n a.length
-- Example expressions:
a = Point 0 0
b = Point 1 1
c = Point 3 3
ab = a.distance b
ac = a.distance c
bc = b.distance c
result_distance = (Point 1 1).distance (Point 10 10)
result_length = b.length
result_div_n = c.div_n c.length
result_norm = c.norm
This approach has been explored in the post Stealing Impl from Rust. Reddit discussion.
See also: https://github.com/ElderEphemera/instance-impl
User Historical_Emphasis7 makes the following observation:
In Example 1. Wouldn't the distance function break if the record was updated?
p = make_point 1.0 1.0 p' = p {y = 10}
This appears to be an issue for the first approach mentioned above as well as the enhanced approach described by friedbrice.
However, the HasField approach appears to work fine with updates.
That said, technically, I haven't been able to test this out due to the following issue:
Updating a record using RecordDotSyntax results in an error
In languages with method-syntax, fluent interfaces are common.
Using the div_n
method from the HasField
approach mentioned above, a method chain in C# might look like this:
var result = c.div_n(2)
.div_n(3)
.div_n(4)
Of course, the naive translation to Haskell:
result =
c.div_n 2
.div_n 3
.div_n 4
doesn't work due to precedence issues.
If we add parenthesis, it get's messy:
result =
(((c.div_n 2)
.div_n 3)
.div_n 4)
and actually still doesn't work because it seems that whitespace is not allowed around the dot!
So, we're forced to do the following it seems:
result = (((c.div_n 2).div_n 3).div_n 4)
Which is defintely quite awkward... This is a huge ergonomic downside for methods-via-dot-syntax.