HN user

T-R

1,353 karma
Posts4
Comments406
View on HN

No, the issue is if the first binding is on the same line as the `let`, you are required to write, e.g.:

    someValue = let f = 9
                    fo = 10
                    foo = 123
      in f+fo+foo
rather than:
    someValue = let f = 9
      fo = 10
      foo = 123
      in f+fo+foo
I think it used to be the case that it had to be indented past the `=` or the `let` even if it was't on the same line. Note also that `in` has to be indented past `someValue`, but doesn't need to be indented as far `let`.

This is fine:

    someValue = let
      f = 9
      fo = 10
      foo = 123
      in f+fo+foo
So, it is possible to land on sane indentation, but the parser is much pickier than, e.g., Python's off-sides rule, so it takes some trial and error for new users to find it, and it can be frustrating if you're just temporarily modifying an expression to quickly try something out.

I honestly think it would be less surprising if the parser just disallowed writing the first binding on the same line as the `let` entirely, treating it only as a block, but some people (bewilderingly) do seem to prefer to write their code with the excessive indentation (I'd imagine with editor support, rather than manually maintaining the spacing).

inner expressions must be indented more

Not to support the parent comment, which I disagree with, but If you use multi-line let-bindings, those require that you indent not just more than the previous line, but as much as the first token after the let keyword on the previous line. It’s a very strange rule, all the more surprising because it’s inconsistent even with the rest of the language. It is totally avoidable if you, like I think most experienced haskellers do, just prefer ‘where’, but people more familiar with procedural code usually lean into using ‘let’ everywhere because it feels more familiar.

I think the strange indentation used to be required in more places - I vaguely remember running into it a lot more when I started with Haskell 20 years ago, but that was also just when I was new to the language. These days I just keep ‘let’ to a bare minimum, so it doesn’t bother me. One thing that made Elm frustrating was that it disallowed ‘where’ clauses, forcing you to deal with this weird edge case all the time.

I'm a huge fan of it - I was never able to get one while they were officially available, I only managed to rent one from Blockbuster a few times. I eventually picked one up on eBay in the early 2000's for $60, before they became collectors items.

The appeal is definitely somewhere between intrinsic and from the rarity - there's not just been no official way to play these games for the last 30 years, they've barely even gotten a passing mention, and even emualators have been few and far between. It was sad that Teleroboxer, Wario Land, and Mario Clash were basically lost to time.

The games definitely weren't up to to NES/SNES quality, but they were at least up to par with the average portable game at the time, which I think is what they were meant to be compared against. I see the Virtual Boy games as Game Boy games that suffered from the mechanics of the platform the platform they were released on. In that light, I think they compare pretty favorably to similar lesser-known games of the time; most WonderSwan games I've played, hell even a lot of Game Gear and Game Boy games, had much more serious gameplay issues, even though they were on hardware that was arguably less... challenging.

The article seems to assume readers are already familiar with the context, or maybe that they'll stop to read the original paper. For those who aren't familiar, Selective Applicative Functors were presented as part of the development of the Haxl library at Facebook (after they hired a series of prominent Haskellers like author of "Real World Haskell", Bryan O'Sullivan, and GHC Co-Developer Simon Marlow). Haxl is a batching framework for Haskell (to, e.g., solve the "N+1 database query problem"), which later inspired the various DataLoader libraries in other language ecosystems.

In Haskell, there's a lot of desire to be able to write effectful code as you normally would, but with different types to do things like restrict the available actions (algebraic effects) or do optimizations like batching. The approaches generally used for this (Free Monads) do this by producing a data structure kind of like an AST; Haskell's "do" notation transforms the sequential code into Monadic "bind" calls for your AST's type (like turning .then() into .flatMap() calls, if you're from Javascript), and then the AST can be manipulated before being interpreted/executed. This works, but it's fundamentally limited by the fact that the "bind" operation takes a callback to decide what to do next - a callback is arbitrary code - your "bind" implementation can't look into it to see what it might do, so there's no room to "look ahead" to do runtime optimization.

Another approach is to slide back to something less powerful than Moands, Applicative Functors, where the structure of the computation is known in advance, but the whole point of using Monads is that they can decide what to do next based on the runtime results of the previous operation - that they accept a callback - so by switching to Applicatives, by definition you're giving up the ability to make runtime choices like deciding not to run a query if the last one got no results.

Selective Functors were introduced as a middle ground - solidifying the possible decisions ahead of time, while still allowing decisions based on runtime information - for example, choosing from a set of pre-defined SQL queries, rather than just running a function that generates an arbitrary one.

Abstract Algebra, looked at through the lens of Programming, is kind of "the study of good library interface design", because it describes different ways things can be "composable", like composing functions `A -> B` and `B -> C`, or operators like `A <> A -> A`, or nestable containers `C<C<T>> -> C<T>`, with laws clearly specifying how to ensure they don't break/break expectations for users, optimizers, etc. Ways where your output is in some sense the same as your input, so you can break down problems, and don't need to use different functions for each step.

Category Theory's approach of "don't do any introspection on the elements of the set" led it to focus on some structures that turned out to be particularly common and useful (functors, natural transformations, lenses, monads, etc.). Learning these is like learning about a new interface/protocol/API you can use/implement - it lets you write less code, use out-of-the-box tools, makes your code more general, and people can know how to use it without reading as much documentation.

Focusing on these also suggests a generally useful way to approach problems/structuring your code - rather than immediately introspecting your input and picking away at it, instead think about the structual patterns of the computation, and how you could model parts of it as transformations between different data structures/instances of well-known patterns.

As a down-to-earth example, if you need to schedule a bunch of work with some dependencies, rather than diving into hacking out a while-loop with a stack, instead model it as a DAG, decide on an order to traverse it (transform to a list), and define an `execute` function (fold/reduce). This means just importing a graph library (or just programming to an interface that the graph library implements) instead of spending your day debugging. People generally associate FP with recursion, but the preferred approach is to factor out the control flow entirely; CT suggests doing that by breaking it down into transformations between data structures/representations. It's hugely powerful, though you can also imagine that someone who's never seen a DAG might now be confused why you're importing a graph library in your code for running async jobs.

Optics let you concisely describe the location, but defer the dereferencing, so you could definitely approximate optics, not by passing around pointers you compute with `offsetof`, but passing around functions that use `offsetof` to return memory locations to reference (read/write to). You could certainly write a composition operator for `*(*T) => List<*R>`... Some people have done something like it[1][2]:

    Account acc = getAccount();
    QVERIFY(acc.person.address.house == 20);

    auto houseLens = personL() to addressL() to houseL();
    std::function<int(int)> modifier = [](int old) { return old + 6; };
    
    Account newAcc2 = over(houseLens, newAcc1, modifier);
These also use templating to get something that still feels maybe a little less ergonomic than it could be, though.

[1] https://github.com/graninas/cpp_lenses [2] https://github.com/jonsterling/Lens.hpp

I definitely agree for traversals, but Lenses need some sort of primitive support - even in Haskell they're mostly generated with TemplateHaskell, and the language developers have spent a long time trying to make the `record.field` accessor syntax overloadable enough to work with lenses[1][2]. Hopefully someday we'll be free from having to memorize all the lens operators.

Optics are famously abstract in implementation, but I don't think people have trouble applying them - people seem to like JQuery/CSS selectors, and insist on `object.field` syntax; it's kind of wild that no mainstream language has a first-class way to pass around the description of a location in an arbitrary data structure.

[1] https://ghc-proposals.readthedocs.io/en/latest/proposals/002...

[2] https://ghc-proposals.readthedocs.io/en/latest/proposals/015...

should I implement the Monad, Applicative or Functor type class?

I struggled with this when I first learned Haskell. The answer is "yes, if you can". If you have a type, and you can think of a sane way to implement `pure`, `fmap`, and `bind` that doesn't break the algebraic laws, then there's really no drawback. Same for any typeclass. It gives users access to utility functions that you might not really have to document (because they follow a standard interface) and you might not even have to maintain (when you can just use `deriving`).

Doing so will let you/users write cleaner code by allowing use of familiar tools like `do` notation, or functions from libraries that say they'll work for any Monad. It saves you from coming up with new names for those functions, and saves users from having to learn them; if I see something's a Monad, I know I can just use `do` notation; if I see something's a Monoid, I know I can get an empty one with `mempty` and use `fold` with it. As long as it's not a really strange Monad, and it doesn't break any laws, it probably just works the way it looks like it does.

If you can define `bind` et. al., but it breaks the laws, it means the abstraction is leaky - things might not work as expected, or they might work subtly differently when someone refactors the code. Probably don't do that.

If you don't implement a typeclass that you could have, it just means you might have written some code where you could've used something out of the box. Same as going through old code and realizing "this giant for-loop could've just been a few function calls if I used underscore/functools or generators".

That said, it's not too common to stumble on a whole new Monad. The Tweet type probably isn't a Monad - what does it mean for a Tweet to be parameterized on another type like `Int`, as in `Tweet<Int>`? What would it mean to `flatMap`(`bind`) a function like `Int -> Tweet<String>` on it? A Tweet is probably just a Tweet. On the other hand, it's a little easier to imagine what a `JSON<Int>` might be, and what applying a function like `Int -> JSON<String>` to it might reasonably do. Or what applying an `Int -> Graph<String>` to a `Graph<Int>` might do.

Most Monads in practice are combinations of well known ones. Usually you'll be writing some procedural code in IO, or working with a parser, and realize "I'm writing a lot of code checking for errors", "I'm tired of explicitly passing this same argument", or "I need some temporary mutable storage", or some other Effect - so you wrap up the Monad you're using with a Monad Transformer like `ExceptT`, `ReaderT`, or `StateT` in a `newtype`, derive a bunch of typeclasses, and then just delete a bunch of messy code.

Thinking too concretely about monads as boxes might make the behavior of the ListT monad transformer seem a bit surprising... unless you were already imagining your box as containing Schrodinger's cat.

I can definitely understand the author taking offense to the interaction, but now that a lot more programmers have had some experience with types like Result<T> and Promise<T> in whatever their other favorite typed language with generics is, the box/container metaphors are probably less helpful for those people than just relating the typeclasses to interfaces, and pointing out that algebraic laws are useful for limiting the leakiness of abstractions.

A good place to start is with the original Game Boy - you can probably gather from the article that, to build a GBA emulator, you need to support some aspects of the Game Boy anyway (notably the sound chip). That would give you a simpler place to start getting familiar with the general concepts (like memory-mapped IO, graphics hardware, etc) and structure (like syncing emulation speed to the sound buffer) as a foundation before jumping into the more complicated GBA.

There's tons of tutorials[1], documentation[2], and tests[3] for building a Game Boy emulator, not to mention existing implementations in every language under the sun. Your favorite LLM has also surely already read them all, so you can have as much hand-holding as you'd like.

[1] https://www.youtube.com/watch?v=e87qKixKFME&list=PLVxiWMqQvh...

[2] https://gbdev.io/pandocs/

[3] https://github.com/c-sp/game-boy-test-roms

Shinjuku station has also changed dramatically from all the construction over the last 10 or so years. I lived in Shinjuku (the ward; I was actually a few stations north on the Oedo line) in 2009/2010; was back there a few weeks ago and it was unrecognizeable. Right now, the whole area over by the Yodobashi Camera, and where they used to have the night bus pickup is all walled off under active construction (if they haven't finished already).

I think, coming from other languages, it takes a while to really absorb how much you can rely on types to tell you what a function is doing - to the degree that for some functions you can infer the implementation from them[1].

That said, Haskell documentation tends to be really lacking on some of the basics that other ecosystems get right, like where to start, shielding beginners from functions for more niche usecases, examples of how to build or deconstruct values, or end-to-end case studies. Some of the nicest things to _use_ have an API that primarily comes from typeclasses in base, and don't provide any documentation beyond a list of typeclasses. My impression has been that most Haskellers seem to be on the same page that those things need work, though - I'm optimistic about the situation improving, but it's still hard to recommend to anyone expecting the experience of working with popular libraries from more mainstream ecosystems.

[1] https://bartoszmilewski.com/2014/09/22/parametricity-money-f...

Doing things in an IO monad, you don't distinguish much between types of effect, everything's just in IO, and you just execute the action when you run into it, which means that you don't have, e.g., lookahead to see if you can do one batch request instead of 10 individual requests. There have been a few attempts to address these -

Monad transformers allow you to separate types of effects (so you can specify e.g., "this code only needs environment variables, not database access"), and, at least at compile time, select a different implementation for each effect. In Haskell, at least, though, they have a drawback of needing to define typeclass instances (interpreters) for every concrete monad stack (basically explicitly describe how they interact with each other - the n-squared instances problem. In practice, there's a bunch of template code to help mitigate the boilerplate).

Somewhat relatedly, Haxl, in an attempt to optimize effects, introduced a compiler change to identify less dynamic code (code that only needed Applicative), and Selective Functors, to allow for more optimization based on what's coming next.

Algebraic Effects (assuming I'm not incorrect to conflate them a bit with free effects/extensible effects) make things more dynamic, so you're instead effectively building the AST you want, and separately interpreting it at runtime. This should let you look at more of the call tree to decide on an execution plan. Since you'd also not be relying solely on the typeclass mechanism to pick an interpretation strategy, you should also be able to more easily describe how the interpreters compose, saving you from all the boilerplate of the transformers approach.

Apache Arrow 4.0 5 years ago

This is what I was trying to get at - using column vectors gives good cache locality and lets you use SIMD for "multiply all of these by this scalar" for each column, and then for "sum all of these" for the resulting rows. I'd imagine it could also let you optimize multiplications into things like bit-shifts with minimal overhead as well, though I have no idea if that's done in practice. Maybe only tangentially related, but I feel like this talk on Halide[0] is really illustrative of the general concepts.

As others have mentioned, for some operations it can also save you from loading whole columns that aren't relevant for your transformation. The compression point in the sibling comment is definitely also relevant, especially for serialization. A whole lot of reasons to use column vectors.

Using "column-major" here might've been terminology abuse; sorry for the confusion.

[0] https://www.youtube.com/watch?v=3uiEyEKji0M

Apache Arrow 4.0 5 years ago

Data science does a lot of SQL-like and linear-algebra-like transformations over a lot of data, and needs it to be reasonably performant. This means you want to do things like minimize overhead of indexing into data, and use things like SIMD instructions/GPU or parallelize work. To do this, you generally want your data in column-major format - organized as objects of arrays, rather than arrays of objects. Dataframe libraries like Pandas (which uses optimized linear algebra libraries like BLAS/LAPACK under the hood, via numpy) and the Spark Dataframe API are for working with columnar data and getting performance via SIMD or parallelization, respectively.

Generally people start off by doing these computations in a series of batch jobs (an "ETL pipeline", orchestrated with something like Airflow), to transform data into whatever shape they ultimately want it in; streaming technologies like Spark Streaming and Kafka can help with incrementally adding new rows to your data, rather than recomputing the whole thing every batch-job run.

Whenever you want to involve multiple systems or multiple libraries in your dataframe transformations, there's potentially a lot of computational overhead in serializing the dataframes or just converting them between memory representations. Arrow is a standardized format, spearheaded by the person who wrote Pandas, that attempts to match the in-memory representation, so that whether you're passing the data between libraries in-memory or writing a file for some other system to read, no unnecessary transformations need to happen to work on the data.

I don't have a problem getting my fingers down everywhere but the corners of the handle, so just pushing a paper towel down there works fine; I wouldn't be surprised if people with bigger hands than mine have more trouble, though. In practice, I've mostly just been throwing it in the dishwasher.

I have one of these; the complaint about the fan being inaccessible is legitimate, but it's otherwise clean, and easy to clean - the tank and the base can be scrubbed by hand or just thrown in the dishwasher, unlike any vaporizing humidifier I've used. I think the other complaints may be regional - and the fact that the proposed solutions all seem to be vaporizing humidifiers (or boiling water) seems telling.

I got one because in Arizona, the water quality's terrible, but the air is bone-dry to the point it turns your skin to sandpaper. The hard water means that vaporizing humidifiers fill the air with white dust that coats everything, because they vaporize the minerals. Evaporative humidifiers don't; the minerals all end up in the filter. And, while an evaporative humidifier has no trouble going through a full tank of water overnight on the lowest settings in AZ, it phsyically can't oversaturate the air like a sauna in the way that a vaporizing humidifier does.

The air in the north east just doesn't get dry enough (maybe in winter, but then you don't have the AC fighting your humidifier), and if boiling a pot of water is what the author's looking for, an evaporative humidifier just doesn't do that.

Comparing floats for equality is not a good idea

This is very true, but the mention of data science is really significant here.

If you're trying to productionize a neural network or something similar, you ideally want to be able to pin down every source of noise to make sure your results are reproducible, so you can evaluate whether a change in your results reflects a change in the data, a change in the code, or just luck from your initial weights - with a big enough network, for all you know it decided that the 10th decimal place of some feature was predictive. I wouldn't be surprised if the author brings up tanh as an example specifically because it's a common activation function.

If you're pinning all your Javascript dependencies, but the native code it dynamically links to gets ripped out from under you, it could completely mislead you about what produces good results. Similarly, if you want to be able to ship a model to run client-side, it would be nice if you could just test it on Node with your Javascript dependencies pinned and be reasonably confident it'll run the same on the browser.

Of course, if you can't do that it's not the end of the world, since you can compensate by doing enough runs, and tests on the target platform, to convince yourself that it's consistent, but it's a lot nicer if things are just as deterministic as possible.

Typedefs 6 years ago

It's for defining types for existing formats, with the goal of generating types for a lot of different languages.

A lot of formats are just defined with very simple types; the machine-readable descriptions don't capture information like "this field could contain a string or an integer", or "the length of this array is dependent on the value in another field". Idris' type system is precise enough to capture that information, so specifying the type in Idris' type system should be enough to generate (at least a first-pass of) types for anything from Java to Haskell by just dropping the information the target language doesn't understand.

At the very least, it would make for some really precise, concise documentation that could save people implementing libraries a lot of time digging through prose.

Edit: Looking at the examples, it looks like it's actually value-level Idris describing Algebraic types, not Idris types, so I'm not sure if it supports dependent quantification.

Haskell Foundation 6 years ago

I don't think it's a bad thing. Like them or not, they have a somewhat controversial history; a lot of people worried that their projects would fork the ecosystem, or give them too much control of it - I wouldn't be surprised if they decided that it would be better for the foundation's goal of being a community effort to not be seen as having a heavy hand in it. Some people have expressed similar concerns about Facebook and IOHK as well, just from the financial influence they might have, even though their haskell projects haven't been as close to critical infrastructure as FPComplete's have.

Kanji are composed from a much smaller set of radicals, so you look it up by the radicals that compose it. Some dictionaries also divide characters up by stroke count. For the past 15+ years, electronic dictionaries with handwriting recognition have been popular, and now you can just use your phone keyboard.

In practice, sometimes you can also kind of guess at the pronunciation (since characters with the same main radical often have a similar pronunciation) and see what autocomplete lists; maybe that's less true outside of the joyo kanji, though.

While I don't think Japan should be judged solely on its fax machines (they're ahead in some places and behind in others), there is a difference with fax machine usage in Japan vs the US - they're a trailing indicator of another bottleneck. Fax machines are so widespread in Japan largely because they ease the process of physically stamping documents with a registered stamp (a hanko), which is used instead of a signature. So far they've failed to digitize them, and it's often cited as a huge cause of bureaucratic slowdown, especially in the pandemic. One of the goals of the new prime minister is to finally start phasing them out.

My understanding is that the current implementation is only groundwork for possible future performance benefits, since there are no changes to the runtime system or garbage collector, and that their current intended use is mostly for allowing library authors to encode safe session/resource handling in the type system. So, new libraries can make it a type error to fail to properly close a connection or release a shared resource.

Simon Peyton Jones talk: https://www.youtube.com/watch?v=t0mhvd3-60Y

is Rust the motivating factor behind linear types cropping up lately?

Philip Wadler wrote "Linear Types Can Change the World!" while he was working on Haskell back in 1990. I vaguely remember there were some mentions of possibly adding it a year or two before Rust was publicly announced, but it seemed like it was going to be far off at the time. I wouldn't be surprised if Rust contributed to a lot of the interest in it - Simon Peyton Jones did specifically say that he had "Rust Envy" for shipping something similar to linear types in one of his talks.

Linear Types can Change the World!: http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.31.5...

TemplateHaskell and QuasiQuotes are very closely related. They're where that square bracket syntax comes from:

    [whamlet|Hello World!|]
I would say that they're closer to the situation you're concerned about - they're very much like C++ templates, and equally controversial for exactly the same reasons. Still, I don't usually think of C++ templates, or Haskell ones, as being a different style per-se, just an advanced feature to use sparingly, mostly just when they'll save you from writing a bunch of boilerplate.

For completeness - Overloaded strings are the Haskell parallel to Python's b-string, f-string, u-string, etc. TypeFamilies are "let me write functions over types", which sounds like it would be dramatic enough to introduce a different coding style, but since it, like other extensions, needs to play nicely with the type inference, it doesn't conflict with other extensions, so things mostly just work as you'd expect. Since extensions generally work nicely together, most people just turn on "the usual" extensions for the whole project (all the more reason we need a new standard), and only add explicit pragmas in files to call out heavyweight things like TemplateHaskell. In tutorials you're more likely to see them all listed at the top, though, to be clear about what's being used.

You say "of course... I'd have to learn one or more flavors/styles of it", and that seems like a reasonable assumption, but it's not accurate in practice - the extensions largely just add features, so you just turn them on if you're using that feature; there's not much of a separate style for coding around not using one. It's like, if your Java project used a Singleton class, you might feel a need to document it, but if it doesn't, it doesn't mean you have a separate "Singleton-less" style, you just didn't feel the need to use one.

Which isn't to say they're not an issue - Haskell really needs a new language standard to clean up that mess, but they don't really fragment the ecosystem the way that, say, the Python 2 vs 3 split did.

Careful decisions can scale with abstractions, if they're made with some rigor.

In Python, for example, whenever I'm doing operations on lists, I need to consider every function in isolation - will this break for an empty list (zip), or will it break when given a generator (anything that traverses twice)? If I pass a callback with side effects, how will it behave differently if someone calls it twice?

In a a more rigorous ecosystem, I can instead only worry about edge cases on classes of functions/data types - In Haskell, when doing the same operations, I can generally assume that traversal functions were written with consideration for functor/foldable/traversable laws, and those laws tell me how I can safely compose or swap them out without breaking expectations around those edge cases. Higher Kinded Types let code be constrained to work strictly with those abstractions, so I mostly just need to worry about "does this typecheck" and, very rarely, "is this an unlawful traversal". With things like linear and dependent types entering this mix, library authors can communicate even more subtle expectations at compile time instead of leaving users to try to anticipate them or find them at runtime - when you do need to explicitly consider edge cases, the turnaround time on discovering them is important.