6 ms·
The thing I like the most about the "as types" approach is that once the thing is wrapped, passing it around has zero boilerplate. In this program below, the o
by jez 7y ago
The thing I like the most about the "as types" approach is that once the thing is wrapped, passing it around has zero boilerplate.
In this program below, the only place that suffers from having created a newtype for the strings is the place where they're created (parseArgs) and where they're used (copyFile). None of the other functions (main, helper1, helper2) ever need to peek inside the type, so there's no boilerplate but still all the type safety.
newtype Source = Source Filename
newtype Dest = Dest Filename
-- Boilerplate at usage
copyFile :: Source -> Dest -> IO ()
copyFile (Source from) (Dest to) = ...
-- Boilerplate at creation
parseArgs :: IO (Source, Dest)
parseArgs = do
[arg1, arg2] <- getArgs
(Source arg1, Dest arg2)
-- Helpers with NO boilerplate
helper1 :: Source -> Dest -> IO ()
helper1 from to = copyFile from to
helper2 :: Source -> Dest -> IO ()
helper2 from to = helper1 from to
main :: IO ()
main = do
(from, to) <- parseArgs
helper2 from to
And by "all the type safety," I mean it's a type error to accidentally swap the args incorrectly to the function call:
(from, to) <- parseArgs
helper2 to from
- raverbashing 7y agoYes but creating the scaffolding is the hard part and getting to the "once the thing is wrapped" takes more time than the other solutions. You could "force" parameter naming, and it would be easier. Types are good but having 10 types of string doesn't make sense.
- eterps 7y ago> Types are good but having 10 types of string doesn't make sense. Not if the strings are clearly distinct (like a name being different from an e-mail address).
- louthy 7y ago> Types are good but having 10 types of string doesn't make sense. It really does make sense. Types like string, int, float, etc. are poorly defined for most things they represent. There's no context and are just one step away from being dynamic, like values in a dynamic language. By enforcing a contextual contract (perhaps with additional constructors that constrain the value) you leverage the type-system to do the hard work of checking for stupid mistakes. On large code-bases this reduction in cognitive complexity makes a whole class of bugs go away. So, 100s of types of string could make sense, it really does depend how many types of thing you have which are distinct and can be represented as a string. But once they're defined, they're not strings any more, they have an internal state which is a string, yes, but their type is not a string.
- bluGill 7y agoThe hard part - at least in languages I've worked with - is how hard it is to create a new string that acts like a string should act but isn't a string. In many (C's typedef) you can create two new types, but nothing stops you from using mixing them up as they freely convert to each other without even a warning. In some object oriented languages you can often create a new string - but only by writing by hand every single member function. Of course none of the above are unsolvable problems. I've never seen anything that actually makes it easy, but I won't claim to have seen everything.
- lonelappde 7y agoYou are conflating dynamic types with weak types. Int is a relatively weak type and HttpPort is a strong type regardless whether static or dynamic
- michaelcampbell 7y ago> 10 types of string It's not. It's 10 types.
- bewo001 7y agoI want to agree, but the problem is that you now need 10 versions for many operations that are in a string library. Hidden assumptions -- like 'replacing a character in a string can never fail' -- may fail (eg in an email address type which only allows only a limited char set). Some constraints can't be expressed without dependent types (say you want to define a URI type that limits the length to 2000 chars total but is composed of variable length components, like the domain name, query strings etc).
- michaelcampbell 7y ago> but the problem is that you now need 10 versions for many operations that are in a string library Do you? Or do you need just the operations for the things that are relevant to the type at hand? Limiting a broad API to the needed interface for the type you're modeling is a GOOD thing.
- Multicomp 7y agoThis is the killer feature that made me sit up and take notice of F#. Add exhaustive pattern matches and I was hooked
- edflsafoiewq 7y agoIn another module, there's the boilerplate of importing Source and Dest.
- masklinn 7y agoThey get brought in alongside the functions. Especially given Haskell (sadly) defaults to unqualified import.
- Shoue 7y agoNot necessarily, you can use coerce or Newtype instances, so you'd import one thing to deal with all the boilerplate wrapper types in all the modules they come from. This is specific to Haskell, but you could mimic it in other languages.
- mlthoughts2018 7y agoIn Python I might do something like this. class Source(str): pass class Dest(str): pass def helper1(a, b): assert isinstance(a, Source) assert isinstance(b, Dest) copyFile(a, b) if __name__ == “__main__”: # obvs use argparse instead and store # directly to Source & Dest objects # but here to be quick: a, b = sys.argv[1:3] a, b = Source(a), Dest(b) helper1(a, b)
- collyw 7y agoYou know how to complicate something very simple. What is the advantage here? As I would prefer to have the authors original problem than this.
- mlthoughts2018 7y agoYou’re assuming I would endorse this as a good approach in Python in the real world. I was merely trying to show how to quickly emulate the Haskell example with less code in Python. Both the Haskell approach and asserting on types in Python seem like wastes of time to me personally, and I’d rather just have copyFile by itself and consenting adults can pass the args they want to pass. Use unit tests to make sure you’re not passing paths the wrong way. Also, what’s with the attitude? “You know how to complicate something very simple” (from ~15 lines of Python?) — geez, you must be a joy to work with.
- lonelappde 7y agoHow is writing extra boilerplate tests better than saying what you mean in the primary code?
- mlthoughts2018 7y agoIt addresses exactly the cases you care about, whereas trying to bake it into designs or type system usually adds heavy restrictions that don’t necessarily have anything to do with your exact use cases. Basically from a You Aren’t Gonna Need It point of view, doing it in tests lets you minimally address the real use case with much less risk of premature or incorrect abstraction. In my experience, premature abstraction is one of the worst problems in business software. To contrast two far ends of the spectrum, minimalistic patchworks of gross hacking are asymmetrically way better than overhead of premature or incorrect abstractions.
- kragen 7y agoLevels of indirection are not a “code smell” but they offer many more opportunities for bugs than parameter list order does.
- lonelappde 7y agoHow?