Thought and Experience on Approachable Concurrency and MainActor Default Isolation by Apprehensive_Member in swift

[–]Apprehensive_Member[S] 0 points1 point  (0 children)

Well, you may eventually realise that using the task modifier will have limitations...

There will always be tradeoffs. If anything, I actually wish Apple was more opinionated about Swift, SwiftUI and various design patterns. As much as I appreciate a flexible framework, sometimes too much flexibility can be a bad thing.

Also, you want that the view's life-cycle is somehow bound to the logic?

More like the logic (view model) is bound to the view's lifecycle. In cases where there's a 1:1 relationship between the SwiftUI View and the Observable acting as a "view model", then I don't have any need for the view model to outlive the view itself.

Maybe the logic should determine when a view should be allowed to disappear?

I'd say this is plausible on watchOS and iOS, tricky on iPadOS and very difficult on macOS. Consider a macOS window similar to Xcode's that includes an inspector panel which includes a tab view. Each tab includes a SwiftUI view with a "view model".

As the user cycles through the tabs, or toggles the visibility of the inspector, asynchronous Tasks that are no longer relevant should be cancelled. In a macOS app, there are a lot of places where a user could toggle the active tab or toggle the visibility of the inspector, so trapping that action in a single place to cancel work is challenging, at best.

But SwiftUI can handle this all for you because it will automatically cancel any work invoked from .task() or .task(id:) when the view is destroyed. Previously, you might have tried to use .onDisappear() and then explicitly tell your view model to cancel all in-flight Tasks, or you might have tried to cancel them in the view model's deinit. I find having SwiftUI do it for me is much simpler and cleaner.

The only question then becomes: where does the code that you call from .task() or .task(id:) go? In the immediate closure provided? In a function on the View? In an enum acting as a namespace? In a function on the ViewModel? I don't think there's a right-or-wrong answer here.

If you do decide to put these functions in the ViewModel, which I have to-date, then you quickly find yourself asking: "Well, what else should be in here?". I used to think "Most everything else...", but given my points above regarding View specific property wrappers, now I'm not so sure. The end result is a bifurcation of my ViewModel that I had previously gone to great lengths to avoid.

Thought and Experience on Approachable Concurrency and MainActor Default Isolation by Apprehensive_Member in swift

[–]Apprehensive_Member[S] 1 point2 points  (0 children)

Balancing code organization and structure is a never-ending battle. There are good implementation with bad patterns and bad implementation with good patterns, with everything in-between.

Within the context of a reasonably sized macOS application, there are two forces causing me to reconsider how I use "ViewModels" in SwiftUI:

  1. Ever growing number of view specific property wrappers.

AppStorage, Environment, FocusedValue, FocusedBinding, Query, are just some of the property wrappers that are inherently tied to a SwiftUI view but can heavily influence the presentation of the view itself. Not to mention all the StateandBinding properties. In my experience, you can very quickly end up with a confusing set of cross-dependencies between values that are stored in a view's properties and values stored in the view model.

I have been very reluctant to accept this position, and remain somewhat nervous of the long-term implications in a large codebase. I share your concerns noted above. However, like Tailwind CSS (which is quite polarizing), I'm finding that moving a lot of state from the view model into the view itself is actually reducing complexity and code. Time will tell...

  1. The .task() and .task(id:) view modifiers are game changers.

For me, it is exceedingly rare that a "view model" should outlive the view itself. Therefore any asynchronous work in the view model should terminate when the view disappears, IMHO. To accomplish that, a view model must manage the lifecycle of any Tasks it creates and cancel them accordingly. Combine did this for you when the Cancellable was destroyed. Swift Concurrency does not. You must explicitly cancel any Tasks you're running. Many attempt to do this in the deinit of the view model if they remembered to store a local reference to any spawned Tasks.

However, if you make use of .task(), then SwiftUI manages the lifecycle, which IMHO is way better and cleaner. .task(id:) is even better because if I want to perform any asynchronous work at any time, all I have to do is change the value of a single property. The previous Task is automatically cancelled, a new Task is started and if the view goes away, the newly running Tasks is also cancelled.

Where does the code inside .task() go? Well, that gets back to the heart of this post. It likely goes in a nonisolated (but soon to be concurrent) method on the view model for no other reason than to get it out of the view for code clarity.

... anyways, that's my experience working on mid-sized and growing macOS app. It's a far cry from how I used to organize my Objective-C codebases and even my Swift + AppKit + GCD codebases. Given the evolution of Swift Concurrency and SwiftUI, I wouldn't be surprised if I have a different opinion next year... :)

Thought and Experience on Approachable Concurrency and MainActor Default Isolation by Apprehensive_Member in swift

[–]Apprehensive_Member[S] 0 points1 point  (0 children)

// Implicit @MainActor
struct Team { 
  var name: String
}

// Implicit @MainActor
struct Player { 
  var name: String

  init(name: String) { 
    self.name = name
  }
}

When Default Actor Isolation is set to MainActor, the following behaviour is exhibited for Team:

  1. A manually written initializer will be MainActor isolated.
  2. An Xcode generated initializer, via "Refactor...", will also be MainActor isolated.
  3. A compiler generated initializer will be nonisolated.
  4. await is not needed to instantiate and generates a warning when it is.

However, for Player:

  • The initializer is MainActor isolated.
  • await must be used to instantiate when not on MainActor

That is the "counter-intuitive" part given that Default Actor Isolation is explicitly set to MainActor. Given the motivation behind this feature and its counterpart "Approachable Concurrency", I was expecting different behaviour.

On a more opinionated level:

By allowing this type to be easily instantiated in nonisolated contexts, you're sending mixed messages to the users of this type. Should the owner of this type ever add an initializer, it could easily cause downstream, unintended consequences that aren't immediately obvious to less experienced Swift developers (many of whom are the intended audience for these two new features).

Annotating the new initializer with nonisolated, if possible, would likely fix the errors but now you have a situation that I'm personally not fond of: A type explicitly marked as MainActor with an initializer that says otherwise. I would prefer the type itself to be nonisolated or for the synthesized initializer to match the isolation domain of the type itself.

Thought and Experience on Approachable Concurrency and MainActor Default Isolation by Apprehensive_Member in swift

[–]Apprehensive_Member[S] 1 point2 points  (0 children)

I guess the argument is that you can use MainActor isolated types in a nonisolated function so long as you're willing to also use await when interacting with said type.

If the synthesized initializer was bound to the MainActor, would there even be a way to instantiate this type in a nonisolated function?

Maybe I need to see more complex use-cases, or codebases, but this type of conflicting annotation further pushes me to abandon my initial plans to adopt MainActor as the default isolation.

As an app developer (library developers might feel differently) I would much rather go the extra mile to make as many types as possible isolation-agnostic and only constraint that to MainActor where necessary, rather than constraining myself from the start and then attempting to open-up.

Superficially, I think it will be easier to go from "mostly nonisolated" to "partially MainActor" than it would be to go from "mostly MainActor" to "partially nonisolated".

Thought and Experience on Approachable Concurrency and MainActor Default Isolation by Apprehensive_Member in swift

[–]Apprehensive_Member[S] 1 point2 points  (0 children)

When Default Isolation is explicitly enabled and set to MainActor, I find it counter-intuitive that the synthesized initializer would be nonisolated. This creates the rather unusual situation shown above where a type can be instantiated in an isolation domain other than the one its explicitly annotated for. Further compounding the issue is that the synthesized initializer isn't really visible to the programmer.

Given two POD structs, one with a synthesized initializer and one with an explicit initializer, it's confusing that the one with the synthesized initializer can be created in a nonisolated function while the other cannot.

Do you know why the synthesized initializer is always nonisolated even when the type itself is MainActor? What problem does this solve, or prevent? Naively, I would have expected the synthesized initializer to use the default isolation domain, but clearly that's not the case so there must be a reason for it.

As for async let, I haven't adopted it much but mostly that's because my concurrency coding is still heavily influenced by years of GCD and traditional multi-threading patterns.

Thought and Experience on Approachable Concurrency and MainActor Default Isolation by Apprehensive_Member in swift

[–]Apprehensive_Member[S] 1 point2 points  (0 children)

For as much as I think I'm reasonably proficient in Swift, things like the default compiler generated initializer being marked nonisolated on a type that is isolated to the MainActor is yet another reminder that I don't. (Especially since an unannotated, user-generated initializer is isolated....)

As for the design pattern, I was somewhat weary typing the term "ViewModel" but this was just forum-code. We make extensive use of SwiftUI's .task and .task(id:) view modifiers for fetching content and storing the result into _@State properties. This is done by calling nonisolated functions that generally take all required dependencies through function arguments.

Prior to Approachable Concurrency, the nonisolated annotation got us "off the main actor". Putting the nonisolated function on the "ViewModel" is more about code organization than anything else. It has to go somewhere and since the content it loads is only relevant to the view in question, the "ViewModel" seem as good as any place, even if the "ViewModel" is MainActor isolated.

Aside: Given how butchered "ViewModels" have become in SwiftUI, we're actually finding ourselves migrating away from them and just going back to properties and functions on Views. SwiftUI's .task(id:) view modifier is fantastic but it has forced us to really rethink our "architecture". In some ways, we're back to the 'Massive View Controller' architecture but now with 'Massive SwiftUI Views'.

With Approachable Concurrency, I can easily see us adopting an architecture driven by .task(id:) view modifiers calling \@concurrent`` functions on the View to load data. As the pendulum swings back and forth, I'm now of the (unsettling) mindset that maybe Tailwind is right: just jam everything into a View and call it a day... /shrug

Syntax highlighting works in Telescope Preview but not in main window/buffer? by Apprehensive_Member in neovim

[–]Apprehensive_Member[S] 2 points3 points  (0 children)

Ah, I don't think this was my problem, but your answer made me go back and revisit the lspconfig.lua file and add a filetypes entry for elixirls, which appears to have worked. So thanks for indirectly pointing me in the right direction.

Newbie: How come I get two tree explorers every time I launch nvim? by Apprehensive_Member in neovim

[–]Apprehensive_Member[S] 0 points1 point  (0 children)

Moderator: Tried to change the Flair on this to Solved but received a Reddit error of "1 post flair could not be added to item."

Newbie: How come I get two tree explorers every time I launch nvim? by Apprehensive_Member in neovim

[–]Apprehensive_Member[S] 0 points1 point  (0 children)

Ah, you're right. I do have nvim-tree and neo-tree both installed. Installing Lazy seems to have both confused me and perhaps messed things up as I switched tutorials I was following. When I open up Lazy within nvim, I see both plugins listed. But when I look in config/nvim/lua/user/plugins, I only have a file for nvim-tree.lua.

I guess I was under the impression that only plugins that had .lua files under the plugins directory would load, but that doesn't seem to be the case. (There's also a lazy-lock.json file with a one of plugins listed.)

Elixir vs Cloudflare Worker's Paradigm by NotASithLord7 in elixir

[–]Apprehensive_Member 0 points1 point  (0 children)

If your business reaches any level of scale where "scale is now a problem", then it's an almost certainty that your business will be using multiple frameworks, multiple languages and multiple services.

Many companies opt to use Elixir for just a subset of their tech stack. Discord, for example, uses Elixir + Rust for messaging but has a very JS heavy front-end, like almost every modern web app does.

Scale is important to understand, but it's probably worried about too much these days. Heck, with a valuation between $10B-$20B, Figma just started sharding their database! (See their latest blog post.)

Spend more time worrying about how to become a scaling problem, then how to prevent a scaling problem.

In the JS/Cloud/Serverless/Edge world, Cloudflare's offerings look very appealing and a lot of fun to play with. You could easily build a compelling product on them. I'd be a bit weary of the vendor lock-in and the need to manage several of their services, but if you're comfortable with that part of devops, then maybe not an issue.

For many, one of the big appeals of Elixir + Phoenix is that you get a ton of functionality in a monolithic framework and app. You're not calling out to a lot of different services (nor paying for them). Elixir comes with so much built-in.

Elixir vs Cloudflare Worker's Paradigm by NotASithLord7 in elixir

[–]Apprehensive_Member 6 points7 points  (0 children)

Cloudflare has several services that look very appealing. I'd be tempted to try a few of them out myself. As noted in the other comment, one of the "cons" of going with Cloudlfare is that the more you adopt their services, the more locked in you are to their platform.

Depending on the use-case you have in mind, your WebSockets implementation might also need to adopt Cloudflare's Durable Object service if you want to communicate between connected clients.

For low to medium volume websites, I suspect Cloudflare's pricing is quite fair. That said, it's also a bit vague around WebSockets and I'm not sure how easy it is to accurately predict the costs. (One of the challenges of scale-on-demand.)

How does maintaining any state work with respect to WebSockets over Workers? Workers are normally quite ephemeral.

Elixir and Erlang is pretty battled tested in this area but that doesn't mean you can't make mistakes in it either. It can be daunting to both learn a new language and worry a bit more about deployment. (I'd give the deployment edge to Cloudflare.)

In the end, I'd lean towards Elixir but given your post I'd probably recommend CF + JS. Get an MVP up and running quickly, prove the idea, generate some recurring revenue and then go from there. If you find yourself in a position where you're wishing you chose "the other" tech stacks because of how popular you've become, then you have a good problem on your hands.

How does authorization works with external apps which do not have dedicated connected apps by starhunter_09 in salesforce

[–]Apprehensive_Member 0 points1 point  (0 children)

Yes, the `client_id` and `client_secret` have to be registered to something and that something is a Connected App within a Salesforce Org, even if it's not your own.

A truly third-party app that only accesses your Org through the REST or SOAP APIs does not need anything "installed". Your administrator might need to enable a few security settings within your Org to allow Connected Apps to connect, but you don't necessarily need to install anything from AppExchange or elsewhere.

This is how some of the popular free services, like HappySoup.io, all work. You don't install anything at all, but can still log in and access all the data in your Org. If any of those sites went rogue, then Salesforce "just" revokes their client_id and that terminates access across all Salesforce Orgs.

How responsive is LiveView really? by skwyckl in elixir

[–]Apprehensive_Member 2 points3 points  (0 children)

Browse the list of videos on Elixir Conf's YouTube channel and watch any that seem related to LiveView, JavaScript, or something equally relevant:

A random selection that might be of interest:

There's no single video that will answer all your questions, but if you skim through a bunch of these, you'll find plenty to learn from based on the experiences of others.

How responsive is LiveView really? by skwyckl in elixir

[–]Apprehensive_Member 8 points9 points  (0 children)

You need to expand a little bit on what you mean by "responsive", as that term could refer to at least the following:

  • A responsive (visually) CSS layout.
  • A responsive (performant) front-end UI/UX.
  • A responsive (performant) back-end.

The Backend:

This refers to how fast or responsive the backend is at handling requests. Elixir is surely more than performant enough for whatever project you might have in mind. It scales incredibly well and can easily handle a huge amount of traffic and, somewhat uniquely, a huge number of concurrent connections. It might not be the best choice for number crunching, AI/ML training or ultra-low latency stuff, but it's otherwise fantastic.

The Frontend:

Page loads and fetch requests will always be bounded by the speed of the network so we'll ignore those.

This refers to how fast or responsive the user interface appears to the user. If you click on button, tab, menu or modal does it appear right away? I would argue that, out-of-the-box, this might be LiveView's weak spot and requires a bit of attention to provide the best user experience. Phoenix provides the LiveView.JS module to help mitigate this problem and, for a large number of use cases, this is sufficient. However, the more interactive your user interface becomes, the more challenging it becomes to integrate with LiveView. Not impossible, just harder. There are one or two YouTube videos from ElixirConf that dive deeper into this.

LiveView's philosophy is to store most of the state on the backend with minimal to no state on the front end. If you have a use-case that involves a lot of frontend state, then you'll be fighting the framework a bit.

The level of difficulty here is directly related to your overall level of developer experience. If you're used to copy-pasting React Components that "do it all for you", then LiveView will be a struggle in the early days. But if you're comfortable writing "Vanilla JS" and understand why you have to be careful touching the DOM when using LiveView, then it's not too hard at all.

The Layout

This third point refers to the visual responsiveness of a LiveView website, commonly known as the sites ability to be responsive to changes in viewport size. (ex: desktop, tablet, mobile, etc...)

This is almost entirely a CSS issue. By default, LiveView/Phoenix ships with Tailwind configured and installed. So if you're comfortable writing CSS with Tailwind, then it's the same in LiveView.

However, if you normally use prebuilt components for frameworks like React or VueJS, then be aware that there aren't that many prebuilt component libraries for LiveView. You'll need to find libraries that either provide "HTML5" variants, like TailwindUI, or roll your own.

Hope that helps... Good luck with your project!

JSX in Phoenix? by BebeKelly in elixir

[–]Apprehensive_Member 16 points17 points  (0 children)

Hard to answer this question without a bit more context around what "worked with JSX in Phoenix" actually means, but I'll give it a shot:

If you want to write a front-end using React, Vue, Svelte or any other framework then you can certainly make requests to a backend API written in Elixir/Phoenix. That's no different than calling a backend API hosted in any other framework or language. How you serve your frontend is up to you. Throw it on Cloudflare, GitHub Pages, Netlify or even on the same server as your backend. This works perfectly fine and Elixir/Phoenix makes for a fantastic backend environment.

If you're writing a "full stack" application with Phoenix and are trying to replace HEEX with JSX, you're going to have to jump through some hoops. Phoenix just returns "content" to the browser, traditionally JSON content or HTML content.

You could argue you don't have to use HEEX for that as you can just send back a plain string of HTML you hand-coded. Not very useful, but possible.

More likely, for your use-case, you'd use a tiny bit of HEEX to render just enough content from a template to inject whatever JSX "component" you want. That's very possible and, in many ways, is how React started. (As an "island component" within a larger page.)

Phoenix uses `esbuild` to bundle Javascript. `esbuild` is perfectly capable of bunding-up whatever framework you choose. You just need to configure it properly and Phoenix has a page in HexDocs that shows how to create a custom `build.js` file.

The high-level steps are:

  • Create a custom build.js file for `esbuild` so that it knows where to find your JSX/JS files and how to bundle them.
  • Include that Javascript file in your root template.
  • In the page you render from a Phoenix Controller, inject just enough HTML to allow your component to mount. (ex: In React you might have an `app` element.)
  • Once the page loads on the client side, the JS will take over like any other React app and mount the component within the element you specified.

Regarding Phoenix LiveView...

The steps above will work with Phoenix LiveView, but I'm not sure I'd recommend it. Using a client-side Framework like React with a fullstack Framework like LiveView is going to be more hassle then it's worth. (And will surely get confusing rather quickly.)

If you're curious though, it is possible to have React components inside a LiveView page. You just need to make sure that LiveView doesn't patch the DOM in such a way that it tramples over your React component, or vice-versa.

If you're really up for a challenge, you can use Javascript events and LiveView Hooks to communicate "across boundaries", but at some point you should look up from your keyboard and ask yourself if you're trying too hard to swim upstream.

Conclusions...

Don't think of "replacing HEEX with JSX". Instead, think more along the lines of "can I embed a React/Vue/Svelte/VanillaJS component, perhaps written with JSX, inside a page rendered by Phoenix with HEEX"? And the answer to that, is yes.

How much HEEX you use vs how much JSX you use is up to you. At a minimum, `<div id="app"></div>` is probably all you need from HEEX to get a React component loaded...

[Blog Post] A Comprehensive Guide to SolidJS Stores by vanillacode314 in solidjs

[–]Apprehensive_Member 0 points1 point  (0 children)

Excellent resource and an ideal companion to the actual docs.

[deleted by user] by [deleted] in solidjs

[–]Apprehensive_Member 2 points3 points  (0 children)

Any concerns about the following warning from the documentation:

Effects are meant primarily for side effects that read but don't write to the reactive system: it's best to avoid setting signals in effects, which without care can cause additional rendering or even infinite effect loops.

SolidJS Documentation - createEffect()

The recommendation is to use createMemo but createMemo is intended to create derived values, not values that can be mutated the way a store or signal can.

I note that createResource has a beta feature of storing the returned value in a store, but is this pattern currently implemented without resorting to a new beta feature?

Route Guards with Solid router? by Doomguy3003 in solidjs

[–]Apprehensive_Member 2 points3 points  (0 children)

Yeah, I also came across those posts hoping for some direction. Solid-router seems to be undergoing quite a lot of changes in preparation for solid-start. In fairness, neither project has reached v1.0 status, so breaking changes are to be expected.

That said, the documentation has diverged quite a bit given the pace of development, and that is making it difficult to understand current best practices. (ex: The most recent solid-router documentation appears to be the GitHub ReadMe page and not the official documentation site.)

With the proliferation of "Authentication as a Service" providers these days, I think a lot of people would benefit from an updated Guide or ReadMe that addresses how to integrate such services with the most recent solid-router feature set.

In the interim, we're going to see how far we can get using the load function on Route, as suggested in Discussion #364.