> In Japanese, an E column kana followed by I sometimes makes a long E, like in 先生 (sen + sei -> sensē).
While it is sometimes difficult to discern the combined E and I sound, especially for non-native speakers, the word 先生 (sensei) is technically pronounced "sensei" and should be spelled that way to distinguish it from words with long E sounds, such as ええ (ee) and お姉さん (oneesan). Similarly, the OU in 東京 (toukyou) and the OO in 大きな (ookina) are different and should be spelled differently. I hope this helps.
Sure, and in a Japanese song, "sensei" can yield four beats or notes SE/N/SE/I.
But spelling out and singing aren't normal speech. Spelling/singing can break apart diphthongs, like NAI becomes NA-I.
生 is not written with い due to the /e:/ having a different sound from that one in from おねえさん. It does not (when you aren't spelling). It is written the way it is for ancient historic reasons.
> Similarly, the OU in 東京 (toukyou) and the OO in 大きな (ookina) are different
Why would you want to confuse the hell out of those learning Japanese by spelling せんせい (sensei) using an E with a macron, a la "sensē," when that is not at all how you spell it or type in phonetically in an IME? Having a one-to-one romanization for each Hiragana phonetic is far more logical for learners, who are essentially the target of romanized Japanese, than creating a Hooked on Phonics version that is completely disconnected from writing reality.
I also think your comment, written in Japanese, saying, "This stupid nonsense isn't going to be of any use to anyone," is both ignorant and uncalled for.
In plain-text romanization, the standard and expected spelling is “sensei.”
That’s the formal, conventional representation, especially for typing and learning.
Phonetically, in natural speech, the vowel often compresses toward a long /e/ sound, so you may hear something closer to sense or sensee depending on context and speaker.
In stylistic writing (e.g. light novels or dialogue), you might occasionally see phonetic renderings to reflect speech, but in formal or instructional contexts, “sensei” remains the correct and expected form.
In short:
• Orthography: sensei
• Phonetics: can vary in actual speech
• Stylistic writing: sometimes bends toward pronunciation
Different layers, different purposes.
I think this may mostly be a case of people talking past each other.
One side is focusing on orthographic convention (how it’s written and typed),
the other on phonetic realization (how it’s actually pronounced in speech).
Those aren’t contradictory claims — they’re just different layers of the same thing.
Hi, ursAxZA. Yes, you're describing an "elision," which is where speakers drop or blur sounds together to make speech more fluid, like the way some people say, "Sup?" when they mean, "What's up?" or replace the T with a glottal stop in the word "mountain," as they do in Utah.
I wholeheartedly agree that it is fine to write things like "Sup?" when appropriate, such as dialogue in a novel. You see this all the time in Japanese TV, books, magazines, manga, etc. However, I disagree that elisions should dictate how we spell words in regular written communication, especially when discussing a tool meant to help non-native Japanese speakers learn the language. And as the parent poster pointed out, when singing, you would sing "se n se i" rather than "se n se e." The same is true of haiku and other instances where the morae (linguistic beats similar to syllables in English) are clearly enunciated.
As I said, sensei is technically four morae and different than "sensē," and, in my opinion, should remain that way in Romaji, it being a writing system and one method for inputting Japanese text.
Thanks for the respectful conversation. I appreciate the points you brought up.
Thanks — and yes, I think we’re essentially aligned now.
Once we separate the layers — orthography, pronunciation, and stylistic rendering — the friction mostly disappears.
Romanization is a writing system with its own conventions;
speech naturally undergoes reductions and elisions;
and creative writing sometimes pulls closer to the spoken register.
Different layers, different functions — and the confusion only arises when they’re collapsed into one.
Sorry, yes. That is my mistake. Hepburn doesn't use any such ē notation. Hepburn preserves えい and ええ as "ei" and "ee", conflating only "ou" and "oo" into ō (when they appear in a combination that denotes the long o:).
Some modern adaptations of his transcription do, however. E.g. Modern Japanese Grammar: A Practical Guide uses the transcription “sensee” (they consistently don’t use macrons in this book: e.g. they use oo for ō, etc.).
Hepburn didn’t write “sensē” himself because it 1880s it was still pronounced “ei”, not “ē”. If it were pronounced like it’s pronounced nowadays, you can bet he’d spell it with ē.
> Having a one-to-one romanization for each Hiragana phonetic is far more logical for learners
It depends on the learner’s (and textbook author’s) goals. Sometimes, having a phonetic transcription of the more common pronunciation is a more important consideration.
Historically, Hepburn’s transcription pre-dates Japanese orthographic reform. He was writing “kyō” back when it was spelled けふ. Having one-to-one correspondence to kana was not a goal.
So writing sensē is kinda on-brand (even if Hepburn didn’t write like this, because in his times it still wasn’t pronounced with long e).
I think most learners probably only pick up maybe 50 words before switching from romaji to kana anyway, so in the grand scheme of things the romanization's correspondence to the kana orthography isn't that important.
I don’t mean to minimize the huge effort by the Gleam team; however, Elixir cannot become Gleam without breaking OTP/BEAM in the same ways Gleam does. As it stands now, Elixir is the superior language between the two, if using the full Erlang VM is your goal.
I didn't know about the statem limitation, I have howerver worked around it with gen server like wrapper, that way all state transitions were handled with gleams type system.
I have been meaning to ask about that on the discord but its one of the ten thousand things on my backlog.
Maybe i could write a gen_event equivalent.. I have some code which does very similar things.
I worked for a company writing Elixir code several years ago. Prior to my arrival, the ignorant architect had deployed Elixir in a way that broke the BEAM (which he viewed as "old and deprecated"). Furthermore, one of the "staff" engineers—instead of using private functions as they're intended—created a pattern of SomePublicModule and SomePublicModule.Private, where he placed all the "private" functions in the SomePublicModule.Private module as public functions so that he could "test them."
I tried almost in vain to fix these two ridiculous decisions, but the company refused to let code fixes through the review process if they touched "well-established, stable code that has been thoroughly tested." After being there for a couple of years, the only thing I was able to fight through and fix was the BEAM issue, which ultimately cost me my job.
My point in all this is that, at least sometimes, it isn't good engineers writing silly code, but rather a combination of incompetent/ignorant engineers making stupid decisions, and company policies that prevent these terrible decisions from ever being fixed, so good engineers have no choice but to write bad code to compensate for the other bad code that was already cemented in place.
> had deployed Elixir in a way that broke the BEAM (which he viewed as "old and deprecated")
I'd love to hear more about this!
> instead of using private functions as they're intended—created a pattern of SomePublicModule and SomePublicModule.Private, where he placed all the "private" functions in the SomePublicModule.Private module as public functions so that he could "test them."
Yeah, this is weird; you can just put your tests in the PublicModule. Or you can just solve this by not testing your private code ;)
He deployed our applications using Kubernetes and refused to implement libcluster. There was something else, too, but I can't recall what it was. It was seven years ago.
> Yeah, this is weird...
I kept telling this developer that you're supposed to test your private functions through your public interfaces, not expose your private functions and hope nobody uses them (which they did), but that fell on deaf ears. He was also a fan of defdeligate and used it EVERYWHERE. Working with that codebase was so annoying.
The issue isn't that OTP isn't a priority for Gleam, but rather that it doesn't work with the static typing Gleam is implementing. This is why they've had to reimplement their own OTP functionality in gleam_otp. Even then, gleam_otp has some limitations, like being unable to support all of OTP's messages, named processes, etc. gleam_otp is also considered experimental at this point.
Having Erlang-style OTP support (for the most part) is very doable, I've written my own OTP layer instead of the pretty shoddy stuff Gleam ships with. It's not really that challenging of a problem and you can get stuff like typed processes (`Pid(message_type)`, i.e. we can only send `message_type` messages to this process), etc. out of it very easily.
This idea that static typing is such a massive issue for OTP style servers and messaging is a very persistent myth, to be honest; I've created thin layers on top of OTP for both `purerl` (PureScript compiled to Erlang) and Gleam that end up with both type-safe interfaces (we can only send the right messages to the processes) and are type-safe internally (we can only write the process in a type-safe way based on its state and message types).
I wholeheartedly agree with you that gleam_otp is janky. Still, actor message passing is only part of the picture. Here are some issues that make static typing difficult in OTP:
• OTP processes communicate via the actor model by sending messages of any type. Each actor is responsible for pattern-matching the incoming message and handling it (or not) based on its type. To implement static typing, you need to know at compile time what type of message an actor can receive, what type it will send back, and how to verify this at compile time.
• OTP's GenServer behaviour uses callbacks that can return various types, depending on runtime conditions. Static typing would require that you predefine all return types for all callbacks, handle type-safe state management, and provide compile-time guarantees when handling these myriad types.
• OTP supervisors manage child processes dynamically, which could be of any type. To implement static typing, you would need to know and define the types of all supervised processes, know how they are going to interact with each other, and implement type-safe restart strategies for each type.
These and other design roadblocks may be why Gleam chose to implement primitives, like statically typed actors, instead of GenServer, GenStage, GenEvent, and other specialized OTP behaviours, full supervisor functionality, DynamicSupervisor, and OTP's Registry, Agent, Task, etc.
OTP and BEAM are Erlang and Elixir's killer features, and have been battle-tested in some of the most demanding environments for decades. I can't see the logic in ditching them or cobbling together a lesser, unproven version of them to gain something as mundane as static typing.
EDIT: I completely missed the word "actor" as the second word in my second sentence, so I added it.
I suppose I was unclear. It is OTP-style `gen_server` processes that I'm talking about.
> OTP processes communicate via the actor model by sending messages of any type. Each actor is responsible for pattern-matching the incoming message and handling it (or not) based on its type. To implement static typing, you need to know at compile time what type of message an actor can receive, what type it will send back, and how to verify this at compile time.
This is trivial, your `start` function can simply take a function that says which type of message you can receive. Better yet, you split it up in `handle_cast` (which has a well known set of valid return values, you type that as `incomingCastType -> gen_server.CastReturn`) and deal with the rest with interface functions just as you would in normal Erlang usage (i.e. `get_user_preferences(user_preference_process_pid) -> UserPreferences` at the top level of the server).
Here is an example of a process I threw together having never used Gleam before. The underlying `gen_server` library is my own as well, as well as the FFI code (Erlang code) that backs it. My point with posting this is mostly that all of the parts of the server, i.e. what you define what you define a server, are type safe in the type of way that people claim is somehow hard:
It's not nearly as big of an issue as people make it out to be; most of the expected behaviors are exactly that: `behaviour`s, and they're not nearly as dynamic as people make them seem. Gleam itself maps custom types very cleanly to tagged tuples (`ThingHere("hello")` maps to `{thing_here, <<"hello">>}`, and so on) so there is no real big issue with mapping a lot of the known and useful return types and so on.
I read the code but I'm not sure I understood all of it (I'm familiar with Elixir, not with Gleam).
For normal matters I do believe that your approach works but (start returns the pid of the server, right?) what is it going to happen if something, probably a module written in Elixir or Erlang that wants to prove a point, sends a message of an unsupported type to that pid? I don't think the compiler can prevent that. It's going to crash at runtime or have to handle the unmatched type and return a not implemented sort of error.
It's similar to static typing a JSON API, then receiving an odd message from the server or from the client, because the remote party cannot be controlled.
> [...] start returns the pid of the server, right?
Yes, `start` is the part you would stick in a supervision tree, essentially. We start the server so that it can be reached later with the interface functions.
> [...] probably a module written in Elixir or Erlang that wants to prove a point, sends a message of an unsupported type to that pid? I don't think the compiler can prevent that. It's going to crash at runtime or have to handle the unmatched type and return a not implemented sort of error.
Yes, this is already the default behavior of a `gen_server` and is fine, IMO. As a general guideline I would advise against trying to fix errors caused by type-unsafe languages; there is no productive (i.e. long-term fruitful) way to fix a fundamentally unsafe interface (Erlang/Elixir code), the best recourse you have is to write as much code you can in the safe one instead.
Erlang, in Gleam code, is essentially a layer where you put the code that does the fundamentals and then you use the foreign function interface (FFI) to tell Gleam that those functions can be called with so and so types, and it does the type checking. This means that once you travel into Erlang code all bets are off. It's really no different to saying that a certain C function can call assembly code.
In my opinion, Elixir and Phoenix will give you a better experience with BEAM and OTP, excellent tooling, a more mature ecosystem, and one of the best web frameworks ever to exist. I think Gleam is cool, but I can't see trading these benefits in for static typing.
To be fair, I can't think of anything I care less about than static typing, so please keep that in mind when entertaining my opinion.
I also preferred dynamic typing, until my complex rails app grew to the point I didn't dare to do any refactoring.
But I didn't switch opinion until I discovered ML type systems, which really allow for fearless refactoring. At occasion there's some battling to satisfy the typesystem, but even with that I'm more productive once the app grows in complexity.
I thought I'd share my experience, not trying to convince anyone ; - )
I'm sorry you had a bad experience, but this doesn't reflect the experience of most Elixir programmers. I'll share my experience as a counterpoint.
> On a production build, stack traces look like Erlang code...
Elixir has the most readable stacktraces of any language I've used. Here's an example (which is color-coded in the terminal for even more clarity and, as you can see, doesn't contain any Erlang code):
== Compilation error in file lib/app_web/live/authentication/settings.ex ==
** (MismatchedDelimiterError) mismatched delimiter found on lib/app_web/live/authentication/settings.ex:96:1:
error: unexpected reserved word: end
│
4 │ on_mount {AppWeb.UserAuth, :require_sudo_mode
│ └ unclosed delimiter
...
96 │ end
│ └ mismatched closing delimiter (expected "}")
│
└─ lib/app_web/live/authentication/settings.ex:96:1
It's easy to see that the issue is on line 4, and that it is a missing curly brace.
> Then you have macros, which make code unmaintainable...
Elixir gives you full access to the AST, making macros extremely easy to read and reason through. The point of the Lisp-style macros Elixir uses is to simplify your code. If your code becomes unmaintainable due to your use of macros, you're probably misusing them. I'd have to see a sample to make that determination, though.
> Running "mix xref graph" on most Elixir projects shows a spaghetti mess.
Spaghetti? It's a simple two-level tree that is in alphabetical order. Here's an example, and it is like this all the way down:
Having developed many Windows apps using Borland's tools in the 80s and 90s, I disagree with this statement for these reasons:
• Mix is one of the best and most integrated build tools/task runners I've used. For example, you can create, migrate, and reset databases, execute tests, lint code, generate projects, compile, build assets, install packages, pull dependencies, etc.
• ExUnit is a great testing framework that handles all kinds of tests in an easy-to-read DSL similar to Ruby's RSpec.
• IEx is a fantastic REPL.
• Elixir's debugging tools are excellent. For example, IEx.pry lets you stop all processes and interact with your system in that frozen state in the REPL. You can watch variables, run functions, and even create new functions on the fly to interact with your data to see how it behaves in different scenarios.
> Building a team around Elixir is hard.
Why is it hard? I've worked exclusively on Elixir projects for both start-ups and large companies with hundreds of engineers for over ten years now, and never had a problem with hiring teams.
> And the documentation for most of the projects you will use is full of noise, with few workable examples, grandiose claims of performance, and fantastic treasures, and the articles are a great read if you want to waste your entire evening.
for any function and see an explanation of what the function does and examples of how to use it.
> Support for massive concurrency is nice, but you are realistically not going to need it...
Elixir supports minute concurrency as well, and yes, I do need it. For example, in Ruby on Rails, which has a GIL, I'd have to use a gem like Sidekiq to push long-running processes into Redis so they can be processed in the background. In Elixir, I can just run them in a separate, concurrent process, which is simple.
Here's an example that takes a collection of users and a function and then runs each user through that function, each in a separate thread:
defmodule ParallelProcessing do
def map(collection_of_users, func) do
collection_of_users
|> Enum.map(&(Task.async(fn -> func.(&1) end)))
|> Enum.map(&Task.await/1)
end
end
Here's the same Elixir code running in a single thread.
defmodule SequentialProcessing do
def map(collection_of_users, func) do
collection_of_users
|> Enum.map(func)
end
end
In the first example, I could have 1 user, 1,000 users, or 1,000,000 users, and this code would run as optimally as possible on all the cores in my CPU or across all the cores in all the CPUs in my multi-server BEAM cluster. There are no extra programs or libraries needed. In the second example, users are processed one at a time, similar to languages like Python, Ruby, JavaScript, PHP, and Perl.
Given the simplicity of writing parallel code in Elixir, why would I limit myself to one CPU core to perform a task when I can use all cores simultaneously?
> Or deal with people that won't stop selling you how great the language is.
The reason they won't stop selling you on Elixir is that Elixir is a fantastic language. I hope you take the time to revisit it in the future. It really is much better than most things out there.
Python allows for memory sharing between threads - which is why a GIL is necessary.
A high-level language with a strict shared-nothing concurrency model doesn't need a GIL... but you naturally can't practically share very large objects between BEAM processes.
1. Regarding Python's GIL: The issue isn't memory sharing between threads. Java and Go allow you to do this, too, but they don't have GILs. The reason Python has a GIL is that it uses reference counting for memory management. If it didn't have a GIL, multiple threads could simultaneously manipulate reference counts, which would lead to memory corruption/leaks.
2. You can share massive "objects" between BEAM processes. For example, if you're running BEAM in a 64-bit environment, you can share maps, structs, and other data structures that are up to 2,305,843,009,213,693,951 bytes in size.
I hope this information helps. I also hope it is correct. I think it is, but I've been wrong before.
While it is sometimes difficult to discern the combined E and I sound, especially for non-native speakers, the word 先生 (sensei) is technically pronounced "sensei" and should be spelled that way to distinguish it from words with long E sounds, such as ええ (ee) and お姉さん (oneesan). Similarly, the OU in 東京 (toukyou) and the OO in 大きな (ookina) are different and should be spelled differently. I hope this helps.
EDIT: Added a comma.
reply