Hacker Newsnew | past | comments | ask | show | jobs | submit | _rend's commentslogin

My personal alternative take to the usual monad tutorial — greatly simplified:

"Functor", "Applicative", and "Monad" are all just generalizations of the concept of `map` and `flatMap`.

  1. Something is a "Functor" if you know how to call `map` on it, nothing more. "I can take a box of things and turn it into a box of other things, 1-to-1". On lists, for example, this is just `map` itself
  2. Something is an "Applicative" if you know how to call `map` on it, but also know how to take a non-boxed value and put it in a box, and also know how to combine boxes in order
  3. Something is a "Monad" if you know how to do all of the above, but also know how to call `flatMap` on it, nothing more. "I can take a box of things, turn each thing into a new box, and then combine them all in order". On lists, for example this is just `concatMap`
There's nothing really more complex to it, besides how you squint at various things (like functions) to fit them into the concept of `map` and `flatMap`.

To answer your questions more directly:

  1. Monads themselves are neither necessary nor sufficient to perform side effects in Haskell; they don't directly enable the effects, but they *do* help place guardrails on the actual unsafe, low-level code which *can* perform the effects, safely and in an ergonomic and composable way
  2. Yes, "Monad" is just a name for a recurring way to approach a problem. Like in most math and programming, a certain repeating pattern was noticed, and given a name. Because of the math origin of the term, you get "Monad" instead of "flat-mappable"
  3. Like any other tool, you reach for a monad when you have a monad-shaped problem. They're just one (powerful) tool for solving certain problems


Perhaps you can also explain what a box is, and what a flatMap is please? Fortunately I know what map is already. Also, what does it mean to combine boxes in order? Thanks!


Sure! "Box" here is used to just abstractly describe a value that contains other values. Let's take a list as an example:

  [1, 2, 3] :: [Int]
Here, the "box" is a list, and inside of it are the values 1, 2, and 3.

As you know, `map` is an operation that converts the values inside of the box into other values; for example, adding 1 to every element:

  [1, 2, 3] :: [Int]
   |  |  |     (+ 1)
   v  v  v
  [2, 3, 4] :: [Int]
But the operation you perform with `map` doesn't need to keep the values of the same type:

  [ 1,   2,   3 ] :: [Int]
    |    |    |      (show)
    v    v    v
  ["1", "2", "3"] :: [String]
The operation can also produce new boxes! Since `String` is actually itself a list (`[Char]`), the result above is the same as

  [  1,     2,     3  ] :: [Int]
     |      |      |       (show)
     v      v      v
  [['1'], ['2'], ['3']] :: [[Char]]
In some cases, you might want to "flatten" this box-of-boxes together. In some languages this operation is called "flatten"; for lists in Haskell, it's called `concat`

  [['1'], ['2'], ['3']] :: [[Char]]
     |      |      |       (concat)
     v      v      v
  [ '1',   '2',   '3' ] :: [Char]
This example isn't terribly motivating, but you can see when you have deeper lists-of-lists how this might be handy:

  [[1,2,3], [4,5,6], [7,8,9]] :: [[Int]]
      |        |        |        (concat)
      v        v        v
  [1, 2, 3, 4, 5, 6, 7, 8, 9] :: [Int]
Here, we took a collection of boxes (`[[Int]]`) and combined them in order (sequentially) to produce a new box (`[Int]`).

What other languages call `flatMap` is just a `map` operation followed by a `flatten` operation. Very roughly, `Functor` gives you "map" (`map`), `Applicative` gives you "flatten" (`concat`), and `Monad` gives you "flatMap" (`concatMap`).

The power of these comes from considering different types of "boxes". `Maybe`, for example, works almost like a list that can contain up to 1 element, and its operations behave pretty much identically. Other types are interesting because how you define their "box-ness" can lead to interesting/useful results. It can be tough to envision how, e.g., a function could look like a "box", but it turns out that you can define rules for it that make it useful. (What does "map" look like for a function? Well, it turns out that mapping a function over another function is already just... function composition!)

You can go a lot deeper into these definitions, and it helps to look at some implementations to grok them better, but the core concepts themselves are not very complicated. The "magic" is in how you define the "boxes".


To expand a bit, too, on how these definitions make side effects easier to represent in Haskell:

One way to represent side effects in a purely functional language is to model them as if they aren't side effects, by representing them as state changes in the "outside world". You don't need to grok the specifics of this, but the definition of the `IO` monad is:

  newtype IO a = IO (State# RealWorld -> (# State# RealWorld, a #))
i.e., it's a "pure" transformation of the "real world".

This allows you to define a "box" called `IO` that represents a computation that can perform a side-effect (by affecting the "real world"), then returning a value.

The real trick to this is that the "box" is entirely opaque to you: unlike a list or a `Maybe` where you know how to reach in and pull values _out_ (e.g., `head`, `last`, `fromJust`, etc.), `IO` doesn't allow you to do this*. Once you have something inside of an `IO` box, it's stuck there.

This means that you can separate the "impure" world from the "pure" world: you can't perform side effects arbitrarily — you're can only do so in an `IO` context that's intentionally "viral".

The functor/applicative/monad rules just make `IO` easier to use and consume:

  1. `Functor` allows you to "map" over the results of a computation
  2. `Applicative` allows you to chain computations together in order so side effects happen in sequence
  3. `Monad` makes it easier to repeatedly chain computations within a single `IO` context (so if you need to perform repeated side effects, you can "stay" in the outer context — `IO a` instead of `IO (IO (IO (IO (... (IO a)))))`)
This is just one way to represent side effects, and the monad rules are only really needed to make this representation ergonomic to actually use.

(*There is technically a way to "escape" the `IO` monad called `unsafePerformIO`, but you basically never need to use this. If you find yourself reaching for it, don't.)


"Go ahead"?


This is awesome! Love to see something new in this space, especially so heavily inspired by QuickSilver. The UI is slick and fast, and the fuzzy matching (and the match UI itself) is excellent.

If you're taking feedback, I've been a >decade-long user of LaunchBar, and I've yet to find another launcher that handles my most common actions quite as well (except maybe Alfred):

1. I launch a ton of URLs directly from LaunchBar, and it's a killer feature for me to be able to start typing a URL (not intending to match anything) and as soon as I type a period, LaunchBar converts the search to a URL (and inserts 'https://' and '.com'). e.g., if I type "abc.", LaunchBar will expand to "https://abc.com" with the ".com" highlighted for replacement (and hitting Return will open the URL immediately). Right now, if I want to do the same with Tuna and my default mode is Fuzzy Mode, I believe I need to hit '"' to enter Text Mode, type the URL, hit Tab, then search for the "Open URL" action (which also won't recognize a "bare" URL without the scheme, so won't show up for, e.g., "abc.com") — but happy to be wrong! I think it'd be swell if it were possible to configure Tuna to, on '.', convert into text mode, automatically insert "https://" and ".com", and automatically pre-populate the "Open URL" action so I could just hit Return to confirm and launch

2. I use the inline calculator a lot, and really like the "auto math" switch when typing digits (and really like the carve-out for 1Password, where typing '1' will show 1Password in fuzzy search instead of switching to the calculator); switching to text mode automatically on numeric input would be really helpful to do the same

3. I have a few custom search templates in LaunchBar I use all the time (several different search engines), and I'm not sure if it's possible to set up something similar directly inside of Tuna yet without writing custom services or an extension

Obviously, this is just how I use LaunchBar, and may not fit in with your vision of Tuna, but figured it might be some helpful food for thought! Thanks for your work on this :)


4. Running uncompiled AppleScripts instead of opening the document in the editor application.

5. Ejecting one or all volumes.

6. Instant Open to profit from setting the system to a short key repeat and being able to open an item by simply dwelling on the last character a little longer.

7. Sub-Search puts large collections like emojis, bookmarks, and music into second row so that selecting primary items like applications and actions stays fast.

8. Instant Send allows me to quickly open the current Finder selection with an app that is not the default viewer or editor.


This is awesome! The design is slick, and it really does feel right at home on a portable device. Appreciate the work that went into this.

Even more so, huge kudos for the performance tuning! The app launches instantly, so much so that I was initially a bit shocked. I'd forgotten what it feels like to tap a button and have something functional appear on screen with zero delay. When even the simplest apps have loading screens (and even built-in Settings/Reminders/Notes/Phone/etc. have a delay), this was really refreshing to see! I legitimately force-quit a bunch of apps on my phone to compare and nothing comes close on cold launch. Great work!


Thank you! I spent a lot of time eliminating launch-time work. Most state is precomputed or lazily initialized after first render. I wanted it to feel like a calculator, not an app.


This is Swift, where Type? is syntax sugar for Optional<Type>. Swift's Optional is a standard sum type, with a lot of syntax sugar and compiler niceties to make common cases easier and nicer to work with.


Right, so it's not like a union type Type | Null. Then naturally it works the same way as in the languages I listed.


I personally much prefer screen width / 1.618, but to each their own


For completeness, this description of alignment is misleading:

> Well, dear reader, this padding is added because the CPU needs memory to be aligned in sets of 4 bytes because it’s optimized in that fashion.

> ...

> Remember: since structs are aligned to 4 bytes, any padding is therefore unnecessary if the size of the struct is a multiple of 4 without the padding.

Individual data types have their own alignment (e.g., `bool`/`char` may be 1, `short` may be 2, `int` may be 4, `long` may be 8, etc.), and the alignment of a compound type (like a struct) defaults to the maximum alignment of its constituent types.

In this article, `struct Monster` has an alignment of 4 because `int` and `float` have an alignment of 4 for the author's configuration. Expanding one of the `int`s to a `long` could increase the alignment to 8 on some CPUs, and removing the `int` and `float` fields would decrease the alignment to 1 for most CPUs.


Also keep in mind that is also all very CPU and compiler specific. Had one compiler where it packed everything at 4/8, usually 8. Not the 1/2/4/8 you would expect. That was because the CPU would just seg fault if you didnt play nice with the data access. The compiler hid a lot of it if you set the packing with offsets and mem moves and shifting. It was clever but slow. So they by default picked a wide enough packing that removed the extra instructions at the cost of using more memory. x86 was by far the most forgiving while at the time I was doing it. ARM was the least forgiving (at least on the platform I was using). With MIPS being OK in some cases but not others.


Some of the Cray hardware was basically pure 64-bit. The systems largely didn’t recognize smaller granularity. I learned a lot of lessons about writing portable C by writing code for Cray systems.


On one of these less forgiving architectures, how does one write programs that read some bytes off the network, bitcast them into a struct, and do something based on that?

On x86 you would use a packed struct that matches the wire protocol.

Wouldn’t this require extra copying if member reads were forced to be aligned?


yep exactly that. I had that exact issue. Junk coming in from a tcp/ppp connection then had to unpack it. Tons of garbage moves and byte offsetting and then making sure you keep the endianness correct too. On the platform I was using luckily memcpy could do most of what I needed. Not the best way to do it but the wildly out of date branch of gcc could do it. Got pretty good at picking junk out of random streams shifting and and/or whatever was needed. Totally useless skill for what I work on these days.


Um ... isn't alignment generally dictated by the platform ABI so that programs compiled by different compilers can be linked together?


The widely used platforms with multiple compilers generally have one or more written down ABIs that the compilers all follow, but more niche platforms frequently have exactly one compiler (often a very out of date fork of gcc) that just does whatever they felt like implementing and may not even support linking together things built by different versions of that one compiler.


We had that exact thing. Our target at the time was about 6 different platforms. 2 of them had very picky compilers/ABI. We were trying to keep it to one codebase with minimal if-def callouts. Learned very quickly not all compilers are the same even thought they may have the same name and version number. Then the std libs are subtly different enough from each other you really have to pay attention to what you are doing.


Ideally yes, but practically there are at least a dozen just for x86.(there's like 3 big ones).


AFAIK alignment doesn't even matter anymore (for CPU data at least) since the 'word size' of a modern CPU is the size of a cache line (32 or 64 bytes?), e.g. unaligned accesses within a 32 or 64 byte block are not different than aligned accesses.

(technically there is still an advantage of items aligned to their size in that such an item can never straddle adjacent cache lines though)

And there's also still tons of different alignment requirements when working with GPU data - and interestingly those alignment requirements may differ from C's alignment rules, so you may need to explicitly use packed structs (which are still not a standard C feature!) with manual padding.


My understanding is that C++ compilers still add padding by default for performance reasons. CPU will have to spend a few cycles to reorganize data that is not aligned in chunks of 4 bytes.


Daniel Lemire did some measuring ~~recently~~ (oops, in 2012):

https://lemire.me/blog/2012/05/31/data-alignment-for-speed-m...

TL;DR: 10% difference on what in 2012 was a low-end CPU, no difference on "new in 2012" CPUs. So my guess is that by now it really doesn't matter anymore :)


Wasn't aware of that, thanks for the link!


> CPU will have to spend a few cycles to reorganize data that is not aligned in chunks of 4 bytes.

That's not true for quite a lot of CPUs. Pretty much all x64 and stuff don't care


I.e. the author is wrong; struct s { char x; }; is not required to be four-byte-aligned. It can have a 1 byte size, and thus alignment.


And bool may be 4, and char may be 2(m68k)!


Not unless you're stuck in the 90s ;) These sizes have all been standardized to 1 byte since C99.


Not the sizes, the alignments.


Same thing in C though, the primitive types have the same alignment as their size.


That is true on some systems, but not a portable assumption by any means.

Edit:

st7: sizeof(uint32_t)=4, aligned(uint32_t)=1

msp430: sizeof(int)=2, alignof(int)= 1

Real old ARM: sizeof(double)=8, alignof(double)=4

OG M68k sizeof(char)=1, but struct align(char)=2


Also, "sizeof(bool) is not required to be 1."

I've seen register-sized bool on systems without free conversion between register sizes.


> Individual data types have their own alignment (e.g., `bool`/`char` may be 1, `short` may be 2, `int` may be 4, `long` may be 8, etc.), and the alignment of a compound type (like a struct) defaults to the maximum alignment of its constituent types.

I will add that this is implementation defined. IIRC the only restriction the standard imposes on the alignment of a struct is that a pointer to it is also a pointer to its first member when converted, meaning its alignment must practically be a multiple of that of its first field.


implementation-defined means your specialized platform can be supported without needing to conform - it does not mean that common knowledge is false for common users


"Implementation-defined" means that there is nothing to conform to as far as the standard is concerned. I have not claimed that "common knowledge is false for common users" or anything to that effect. My comment is additive, which should have been clear to anyone reading the first three words of it.


> Which means if you actually edited those files, you might fill up your HD much more quickly than you expected.

I'm not sure if this is what you intended, but just to be sure: writing changes to a cloned file doesn't immediately duplicate the entire file again in order to write those changes — they're actually written out-of-line, and the identical blocks are only stored once. From [the docs](^1) posted in a sibling comment:

> Modifications to the data are written elsewhere, and both files continue to share the unmodified blocks. You can use this behavior, for example, to reduce storage space required for document revisions and copies. The figure below shows a file named “My file” and its copy “My file copy” that have two blocks in common and one block that varies between them. On file systems like HFS Plus, they’d each need three on-disk blocks, but on an Apple File System volume, the two common blocks are shared.

[^1]: https://developer.apple.com/documentation/foundation/file_sy...


The key is “unmodified” and how APFS knows or doesn’t know whether they are modified. How many apps write on block boundaries or even mutate just in disk data that has changed vs overwriting or replacing atomically? For most applications there is no benefit and a significant risk of corruption.

So APFS supports it, but there is no way to control what an app is going to do, and after it’s done it, no way to know what APFS has done.


For apps which write a new file and replace atomically, the CoW mechanism doesn't come into play at all. The new file is a new file.

I don't understand what makes you think there's a significant risk of corruption. Are you talking about the risk of something modifying a file while the dedupe is happening? Or do you think there's risk associated with just having deduplicated files on disk?


The vast majority of apps using structured data and not block oriented data formats. A major exception is databases, but common file formats that most people work with - images, text, etc. often aren't best mutated directly on disk, but rewritten either to the same file or a new file. Without some transactional capability, mutating a file directly on disk can corrupt a file if the writer fails in the middle of the write. More than a few text editors use this as their method of saving to ensure that there is never an inconsistent state of that file on disk.


Thanks for the clarification!


> Award-winning photo editing, graphic design and page layout software for Mac, Windows & iPad.

They've supported Windows and iPad for years, too.


Not at all: unless a license is provided, the code is fully protected under copyright and you have _no_ rights to copy it or use it in _any_ way you want (unless falling under "fair use" clauses for the jurisdiction you're in/the author is in).


Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: