Logs: freenode/#haskell
| 2021-05-06 07:23:46 | <olle> | jacks2: depends on where you put the dependencies of functions draw and save. |
| 2021-05-06 07:25:03 | <olle> | tdammers: cool |
| 2021-05-06 07:25:54 | <tdammers> | anyway, OOP languages don't explicitly distinguish effectful code from pure code, but if you take "modern best practices" and run with them, you usually end up with 3 types of classes: those that represent data structures (and have no behavior beyond getters / setters / constructor / destructor), those that are just collections of utility functions (and have no state of their own, except maybe caches / |
| 2021-05-06 07:25:56 | <tdammers> | memoization), and those that implement effectful interactions with the outside world |
| 2021-05-06 07:26:12 | × | nineonin_ quits (~nineonine@2604:3d08:7783:f200:98ea:cd24:b2e0:9b6f) (Remote host closed the connection) |
| 2021-05-06 07:26:17 | <tdammers> | well, that's basically what you'd have in Haskell too. data structures, pure logic, and effects, neatly separated |
| 2021-05-06 07:26:44 | <olle> | Yeah |
| 2021-05-06 07:26:46 | <jacks2> | data Shape = Shape { shapeName :: Text, shapeData :: [Point], shapeDraw :: Shape -> IO () }. then mkCircle points = Shape "Circle" points drawCircle |
| 2021-05-06 07:27:44 | → | waleee-cl joins (uid373333@gateway/web/irccloud.com/x-udyzskrqxqzkzbjc) |
| 2021-05-06 07:27:48 | <tdammers> | jacks2: I'd ditch the shapeDraw member, just use the shapeName (or use an enum type) |
| 2021-05-06 07:28:03 | <tdammers> | defunctionalize |
| 2021-05-06 07:28:39 | <jacks2> | tdammers, and use what instead? with this you can have a list of shapes and then draw them all |
| 2021-05-06 07:29:03 | <jacks2> | this is assuming they can't be drawn just using shapeData |
| 2021-05-06 07:29:06 | <tdammers> | shapeDraw :: Shape -> IO (); shapeDraw s = case shapeName s of { ... } |
| 2021-05-06 07:29:06 | <jacks2> | in a generic way |
| 2021-05-06 07:29:27 | <jacks2> | tdammers, that doesn't meet his requirement.. you can't add different shapes, as a library user |
| 2021-05-06 07:29:31 | <tdammers> | only reason why you'd inject the drawing function into the data structure is because you need the consumer to push their own drawing routines |
| 2021-05-06 07:30:06 | <tdammers> | but if you do that, why not just `type Shape = IO ()` |
| 2021-05-06 07:31:16 | <jacks2> | that's assuming that there's only one "method" each shape can have, drawShape, and that other fields, like name, aren't useful |
| 2021-05-06 07:31:31 | <olle> | saveShape too |
| 2021-05-06 07:31:46 | <tdammers> | sure. but you basically just invented half of OOP |
| 2021-05-06 07:31:51 | <olle> | haha |
| 2021-05-06 07:32:17 | → | Gurkenglas_ joins (~Gurkengla@unaffiliated/gurkenglas) |
| 2021-05-06 07:32:33 | <jacks2> | you can get pretty far with a records of functions. and it meets his requirement, your suggestion doesn't, you are hardcoding a limited number of shapes in your shapeDraw above |
| 2021-05-06 07:32:56 | <tdammers> | I'm questioning the requirement |
| 2021-05-06 07:33:22 | <olle> | Sure, sure :) |
| 2021-05-06 07:33:28 | <tdammers> | IME, this kind of flexibility (allowing truly arbitrary implementations of the draw method) is hardly ever needed |
| 2021-05-06 07:33:37 | <tdammers> | when it is, sure, break out the OOP |
| 2021-05-06 07:34:36 | <olle> | It's also related to encapsulation - if the data for each shape should or should not be public to the outside. |
| 2021-05-06 07:35:04 | <tdammers> | encapsulation is something you achieve by limiting module exports |
| 2021-05-06 07:35:05 | → | coot joins (~coot@37.30.58.122.nat.umts.dynamic.t-mobile.pl) |
| 2021-05-06 07:35:30 | <olle> | tdammers: so you get package private stuff? |
| 2021-05-06 07:35:55 | <tdammers> | you can, if you don't expose private modules, but that's recommended against |
| 2021-05-06 07:36:03 | <tdammers> | you do get module-private stuff though |
| 2021-05-06 07:36:26 | <olle> | But one module = one file...? Or that's only OCaml? |
| 2021-05-06 07:37:08 | <tdammers> | according to the language standard, you can have multiple modules in one Haskell source file, but in practice, GHC doesn't support that and requires one module per file |
| 2021-05-06 07:37:15 | <tdammers> | it also requires the filename to match the module name |
| 2021-05-06 07:37:27 | <tdammers> | (except for Main, that is) |
| 2021-05-06 07:38:31 | <olle> | Imagine we have one file for Point, Rectangle, and another file for shapeSave. Then we want only shapeSave to have access to internal representation of the shapes, but nothing else. Possible? |
| 2021-05-06 07:38:40 | <olle> | Might not matter so much when immutability is the default, tho. |
| 2021-05-06 07:39:25 | <tdammers> | forget "internal representation" |
| 2021-05-06 07:40:00 | <tdammers> | that's not important; what is "internal" is not a property of the data, the data just is, it's a property of the operations acting on the data |
| 2021-05-06 07:40:14 | <tdammers> | "does this function need to know about the points in this shape?" |
| 2021-05-06 07:40:28 | <tdammers> | if it doesn't, then *for the purpose of that function*, the points are "internal" |
| 2021-05-06 07:40:45 | <tdammers> | but you don't need to hide them from the module that defines them, if other functions may need to look at them |
| 2021-05-06 07:40:59 | <tdammers> | you can also "hide" things behind a typeclass, for example |
| 2021-05-06 07:41:10 | <olle> | hmmm |
| 2021-05-06 07:41:16 | <tdammers> | drawShape :: Drawable s => s -> IO () |
| 2021-05-06 07:41:31 | <olle> | Same with Savable? |
| 2021-05-06 07:41:32 | <tdammers> | now all drawShape knows about the shape in question is whatever methods the Drawable typeclass exposes |
| 2021-05-06 07:41:32 | × | cole-h quits (~cole-h@c-73-48-197-220.hsd1.ca.comcast.net) (Ping timeout: 268 seconds) |
| 2021-05-06 07:41:35 | <tdammers> | sure |
| 2021-05-06 07:42:25 | × | hypercube quits (~hypercube@2603-6011-f901-9e5b-0000-0000-0000-08cf.res6.spectrum.com) (Ping timeout: 250 seconds) |
| 2021-05-06 07:42:33 | <tdammers> | and now we get back to "defunctionalization": instead of making the drawing and saving code a member of the Shape type directly, you defunctionalize it. design a little EDSL that captures the primitives you need to express drawing an arbitrary shape, or saving an arbitrary shape |
| 2021-05-06 07:43:01 | <dibblego> | https://en.wikipedia.org/wiki/Expression_problem |
| 2021-05-06 07:43:02 | <olle> | EDSL? Domain-specific? |
| 2021-05-06 07:43:09 | <jacks2> | that works.. but if you wanted to have a list of different shapes, then you also have to resort to use existential types. and you can't have more than one drawing method per shape. records of functions make the code simpler and more flexible |
| 2021-05-06 07:43:17 | <jacks2> | to using existential types* |
| 2021-05-06 07:44:10 | <tdammers> | e.g., data DrawPrim = MoveTo Point | LineTo Point | BezierTo Point Point Point | FillPath. Now you can say data Shape = Shape { ..., shapeDrawPrims :: [DrawPrim] } |
| 2021-05-06 07:44:28 | <tdammers> | and separately: drawPrims :: [DrawPrim] -> IO () |
| 2021-05-06 07:45:17 | <tdammers> | jacks2: I like to think of those records-of-functions as the Haskell equivalent of an OOP interface |
| 2021-05-06 07:45:30 | <olle> | jacks2: right, that's the next use-case: forall shapes as shape, do draw shape |
| 2021-05-06 07:46:02 | <tdammers> | also, relevant: https://lukepalmer.wordpress.com/2010/01/24/haskell-antipattern-existential-typeclass/ |
| 2021-05-06 07:46:23 | × | pavonia quits (~user@unaffiliated/siracusa) (Quit: Bye!) |
| 2021-05-06 07:46:47 | × | vicfred quits (vicfred@gateway/vpn/mullvad/vicfred) (Quit: Leaving) |
| 2021-05-06 07:47:48 | <jacks2> | tdammers, yes.. I see him replacing typeclass with records of functions :) |
| 2021-05-06 07:48:25 | <tdammers> | the gist of it: record-of-functions works and gives you maximum flexibility, but you lose a fair degree of type-level expressivity. however, existentials don't buy you that back, because you're essentially still doing the same thing, only with more boilerplate. |
| 2021-05-06 07:48:50 | <tdammers> | and because you lose that bit of type safety, record-of-functions is generally my last resort |
| 2021-05-06 07:49:13 | <tdammers> | it is sometimes needed, but more often than not, it's actually more prudent to challenge the flexibility requirements |
| 2021-05-06 07:49:18 | <olle> | Which type-safety do you lose with records of functions? |
| 2021-05-06 07:49:30 | <jacks2> | you have just one type, Shape |
| 2021-05-06 07:49:56 | <olle> | jacks2: But shape can have an ADT do say which type of shape it is? |
| 2021-05-06 07:50:02 | <olle> | shapeType = Point | Rectangle |
| 2021-05-06 07:50:27 | <tdammers> | if you do that, you lose the flexibility of adding arbitrary shapes after the fact |
| 2021-05-06 07:50:29 | <jacks2> | you can stick drawRectangle into a circle shape |
| 2021-05-06 07:50:48 | <olle> | Right |
| 2021-05-06 07:51:06 | <olle> | Wonder how the Rust people would approach this... |
| 2021-05-06 07:51:29 | <tdammers> | the problem is the same regardless of programming language, really |
| 2021-05-06 07:51:45 | <olle> | Yes, but each language has its own "idiomatic" solution :) |
| 2021-05-06 07:51:53 | <tdammers> | because the economics are different |
| 2021-05-06 07:52:39 | <davve> | the best languages are the ones who force a specifc code style imo |
| 2021-05-06 07:52:42 | davve | flees |
| 2021-05-06 07:53:58 | <olle> | davve: design patterns and architectural habits change more often than language features. |
| 2021-05-06 07:54:01 | <tdammers> | Elm does that, and it's absolutely terrible for it |
| 2021-05-06 07:55:06 | <tdammers> | PHP also does it, but not on purpose, and it's also terrible for it |
| 2021-05-06 07:55:12 | <davve> | my point is that its good to leave as little room for discussing stuff like indentation |
| 2021-05-06 07:55:15 | <DigitalKiwi> | ghc error if it doesn't pass hindent |
| 2021-05-06 07:55:38 | <DigitalKiwi> | -Werror -Wall -Whindent |
| 2021-05-06 07:55:56 | <tdammers> | if people want to shave yaks, they will. not the language's job to play police. |
| 2021-05-06 07:55:57 | <DigitalKiwi> | -Werror -Wall -Whindent -Whlint |
| 2021-05-06 07:56:44 | <DigitalKiwi> | https://github.com/cachix/pre-commit-hooks.nix |
| 2021-05-06 07:57:00 | <DigitalKiwi> | domenkozar[m] the yak shaver |
| 2021-05-06 07:57:16 | → | m0rphism joins (~m0rphism@HSI-KBW-085-216-104-059.hsi.kabelbw.de) |
| 2021-05-06 07:57:25 | → | merijn joins (~merijn@83-160-49-249.ip.xs4all.nl) |
| 2021-05-06 07:58:49 | → | hypercube joins (~hypercube@2603-6011-f901-9e5b-0000-0000-0000-08cf.res6.spectrum.com) |
| 2021-05-06 08:00:35 | <xsperry> | olle, when it comes to extensibility, functional and OOP modeling work in the opposite direction. in functional languages it is easier to add new functions and harder to add new "types/classes/cases", and the opposite is true for OOP languages. google for "expression problem" |
| 2021-05-06 08:01:32 | <DigitalKiwi> | or click the link dibblego sent lol |
| 2021-05-06 08:01:50 | <davve> | imo functional makes the most sense unless you are modelling stuff from real life, then encapsulating data, mutations and stuff are more natural |
| 2021-05-06 08:02:11 | <jacks2> | types ala carte supposedly address the expression problem? I never explored them |
| 2021-05-06 08:02:26 | <tdammers> | we are always modelling stuff from real life. or never, depending how you look at it. |
All times are in UTC.