世界最髒河水可飲用 印尼拚2025乾淨家園

摘錄自2019年11月18日中央通訊社報導

西爪哇的西大魯河曾被稱為世界上最髒的河流,印尼從2年前整治至今,部分上游河水已可供居民飲用。印尼官員說,乾淨印尼是全國性運動,將持續動員全民達到2025年的目標。

印尼總統佐科威(Joko Widodo)在2017年宣示,2025年將達成讓印尼乾淨的目標(Gerakan Indonesia Bersih),包括垃圾從源頭減量3成、並處理7成的垃圾,以避免垃圾直接放置於堆積場或流入海洋。

這是印尼首度、也是唯一動員軍隊清理的河川。負責清理的少將蘇山托(Susanto)之前表示,上游整治後,經養魚測試水質6個月前設置第一台淨水機器,已開始供居民使用。

印尼人口2億6700萬,因垃圾清運不普及、缺乏處理機制,也沒有回收概念,大量垃圾不是丟河裡,就是掩埋或燒掉。許多都市貧民社區及鄉下民眾取用浸泡垃圾的河水作日常使用,暴露在遭垃圾污染的環境及空氣中,衛生條件極差。

輿論普遍分析,印尼乾淨運動是遠大、有難度的目標。印尼海洋事務統籌部海洋科學與科技主任納尼說,政府已列出67條優先治理的河川,以減少海洋垃圾;密集與地方政府合作推動減量,目前已有峇里島等12個地方政府制定限制使用一次性塑膠產品;也研擬嚴懲與觀光有關的行為造成的垃圾污染;並尋求新科技的運用。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

新北清潔公司,居家、辦公、裝潢細清專業服務

減塑全球風潮 2020年起泰國商場禁用塑膠袋

摘錄自2019年11月18日NOWnews報導

為了達到環保的目的,2020年起泰國許多連鎖超商或大賣場將不再供應塑膠袋。根據路透社的報導,泰國國家資源和環境部部長瓦拉伍特(Varawut Silpa-archa)在臉書宣佈,許多商店與政府合作,暫停提供一次用塑膠袋,措施將於2020年1月起生效。

「方便性會下降,不過這是為了延續環境的生態循環。」瓦拉伍特受訪時說,他建議民眾可以使用可重複利用的大布袋取代塑膠袋。

不再提供塑膠袋的商店包括在泰國管理上千家7-11的CP All Plc公司和泰國購物商場集團。其實,這並不是泰國第一次嘗試縮減塑膠袋使用的政策。早在2018年,CAN便報導過泰國旅遊局曾發佈「國家淨化聲明」,希望在2021年時,能減少一半以上現行使用的塑膠袋總用量。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※幫你省時又省力,新北清潔一流服務好口碑

※別再煩惱如何寫文案,掌握八大原則!

Spring Boot 2.x基礎教程:MyBatis的多數據源配置

前两天,我們已經介紹了關於JdbcTemplate的多數據源配置以及Spring Data JPA的多數據源配置,接下來具體說說使用MyBatis時候的多數據源場景該如何配置。

添加多數據源的配置

先在Spring Boot的配置文件application.properties中設置兩個你要鏈接的數據庫配置,比如這樣:

spring.datasource.primary.jdbc-url=jdbc:mysql://localhost:3306/test1
spring.datasource.primary.username=root
spring.datasource.primary.password=123456
spring.datasource.primary.driver-class-name=com.mysql.cj.jdbc.Driver

spring.datasource.secondary.jdbc-url=jdbc:mysql://localhost:3306/test2
spring.datasource.secondary.username=root
spring.datasource.secondary.password=123456
spring.datasource.secondary.driver-class-name=com.mysql.cj.jdbc.Driver

說明與注意

  1. 多數據源配置的時候,與單數據源不同點在於spring.datasource之後多設置一個數據源名稱primary和secondary來區分不同的數據源配置,這個前綴將在後續初始化數據源的時候用到。
  2. 數據源連接配置2.x和1.x的配置項是有區別的:2.x使用spring.datasource.secondary.jdbc-url,而1.x版本使用spring.datasource.secondary.url。如果你在配置的時候發生了這個報錯java.lang.IllegalArgumentException: jdbcUrl is required with driverClassName.,那麼就是這個配置項的問題。
  3. 可以看到,不論使用哪一種數據訪問框架,對於數據源的配置都是一樣的。

初始化數據源與MyBatis配置

完成多數據源的配置信息之後,就來創建個配置類來加載這些配置信息,初始化數據源,以及初始化每個數據源要用的MyBatis配置。

這裏我們繼續將數據源與框架配置做拆分處理:

  1. 單獨建一個多數據源的配置類,比如下面這樣:
@Configuration
public class DataSourceConfiguration {

    @Primary
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.primary")
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.secondary")
    public DataSource secondaryDataSource() {
        return DataSourceBuilder.create().build();
    }

}

可以看到內容跟JdbcTemplate、Spring Data JPA的時候是一模一樣的。通過@ConfigurationProperties可以知道這兩個數據源分別加載了spring.datasource.primary.*spring.datasource.secondary.*的配置。@Primary註解指定了主數據源,就是當我們不特別指定哪個數據源的時候,就會使用這個Bean真正差異部分在下面的JPA配置上。

  1. 分別創建兩個數據源的MyBatis配置。

Primary數據源的JPA配置:

@Configuration
@MapperScan(
        basePackages = "com.didispace.chapter39.p",
        sqlSessionFactoryRef = "sqlSessionFactoryPrimary",
        sqlSessionTemplateRef = "sqlSessionTemplatePrimary")
public class PrimaryConfig {

    private DataSource primaryDataSource;

    public PrimaryConfig(@Qualifier("primaryDataSource") DataSource primaryDataSource) {
        this.primaryDataSource = primaryDataSource;
    }

    @Bean
    public SqlSessionFactory sqlSessionFactoryPrimary() throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(primaryDataSource);
        return bean.getObject();
    }

    @Bean
    public SqlSessionTemplate sqlSessionTemplatePrimary() throws Exception {
        return new SqlSessionTemplate(sqlSessionFactoryPrimary());
    }

}

Secondary數據源的JPA配置:

@Configuration
@MapperScan(
        basePackages = "com.didispace.chapter39.s",
        sqlSessionFactoryRef = "sqlSessionFactorySecondary",
        sqlSessionTemplateRef = "sqlSessionTemplateSecondary")
public class SecondaryConfig {

    private DataSource secondaryDataSource;

    public SecondaryConfig(@Qualifier("secondaryDataSource") DataSource secondaryDataSource) {
        this.secondaryDataSource = secondaryDataSource;
    }

    @Bean
    public SqlSessionFactory sqlSessionFactorySecondary() throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(secondaryDataSource);
        return bean.getObject();
    }

    @Bean
    public SqlSessionTemplate sqlSessionTemplateSecondary() throws Exception {
        return new SqlSessionTemplate(sqlSessionFactorySecondary());
    }

}

說明與注意

  1. 配置類上使用@MapperScan註解來指定當前數據源下定義的Entity和Mapper的包路徑;另外需要指定sqlSessionFactory和sqlSessionTemplate,這兩個具體實現在該配置類中類中初始化。
  2. 配置類的構造函數中,通過@Qualifier註解來指定具體要用哪個數據源,其名字對應在DataSourceConfiguration配置類中的數據源定義的函數名。
  3. 配置類中定義SqlSessionFactory和SqlSessionTemplate的實現,注意具體使用的數據源正確(如果使用這裏的演示代碼,只要第二步沒問題就不需要修改)。

上一篇介紹JPA的時候,因為之前介紹JPA的使用時候,說過實體和Repository定義的方法,所以省略了 User 和 Repository的定義代碼,但是還是有讀者問怎麼沒有這個,其實都有說明,倉庫代碼里也都是有的。未避免再問這樣的問題,所以這裏就貼一下吧。

根據上面Primary數據源的定義,在com.didispace.chapter39.p包下,定義Primary數據源要用的實體和數據訪問對象,比如下面這樣:

@Data
@NoArgsConstructor
public class UserPrimary {

    private Long id;

    private String name;
    private Integer age;

    public UserPrimary(String name, Integer age) {
        this.name = name;
        this.age = age;
    }
}

public interface UserMapperPrimary {

    @Select("SELECT * FROM USER WHERE NAME = #{name}")
    UserPrimary findByName(@Param("name") String name);

    @Insert("INSERT INTO USER(NAME, AGE) VALUES(#{name}, #{age})")
    int insert(@Param("name") String name, @Param("age") Integer age);

    @Delete("DELETE FROM USER")
    int deleteAll();

}

根據上面Secondary數據源的定義,在com.didispace.chapter39.s包下,定義Secondary數據源要用的實體和數據訪問對象,比如下面這樣:

@Data
@NoArgsConstructor
public class UserSecondary {

    private Long id;

    private String name;
    private Integer age;

    public UserSecondary(String name, Integer age) {
        this.name = name;
        this.age = age;
    }
}

public interface UserMapperSecondary {

    @Select("SELECT * FROM USER WHERE NAME = #{name}")
    UserSecondary findByName(@Param("name") String name);

    @Insert("INSERT INTO USER(NAME, AGE) VALUES(#{name}, #{age})")
    int insert(@Param("name") String name, @Param("age") Integer age);

    @Delete("DELETE FROM USER")
    int deleteAll();
}

測試驗證

完成了上面之後,我們就可以寫個測試類來嘗試一下上面的多數據源配置是否正確了,先來設計一下驗證思路:

  1. 往Primary數據源插入一條數據
  2. 從Primary數據源查詢剛才插入的數據,配置正確就可以查詢到
  3. 從Secondary數據源查詢剛才插入的數據,配置正確應該是查詢不到的
  4. 往Secondary數據源插入一條數據
  5. 從Primary數據源查詢剛才插入的數據,配置正確應該是查詢不到的
  6. 從Secondary數據源查詢剛才插入的數據,配置正確就可以查詢到

具體實現如下:

@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class Chapter39ApplicationTests {

    @Autowired
    private UserMapperPrimary userMapperPrimary;
    @Autowired
    private UserMapperSecondary userMapperSecondary;

    @Before
    public void setUp() {
        // 清空測試表,保證每次結果一樣
        userMapperPrimary.deleteAll();
        userMapperSecondary.deleteAll();
    }

    @Test
    public void test() throws Exception {
        // 往Primary數據源插入一條數據
        userMapperPrimary.insert("AAA", 20);

        // 從Primary數據源查詢剛才插入的數據,配置正確就可以查詢到
        UserPrimary userPrimary = userMapperPrimary.findByName("AAA");
        Assert.assertEquals(20, userPrimary.getAge().intValue());

        // 從Secondary數據源查詢剛才插入的數據,配置正確應該是查詢不到的
        UserSecondary userSecondary = userMapperSecondary.findByName("AAA");
        Assert.assertNull(userSecondary);

        // 往Secondary數據源插入一條數據
        userMapperSecondary.insert("BBB", 20);

        // 從Primary數據源查詢剛才插入的數據,配置正確應該是查詢不到的
        userPrimary = userMapperPrimary.findByName("BBB");
        Assert.assertNull(userPrimary);

        // 從Secondary數據源查詢剛才插入的數據,配置正確就可以查詢到
        userSecondary = userMapperSecondary.findByName("BBB");
        Assert.assertEquals(20, userSecondary.getAge().intValue());
    }

}

代碼示例

本文的相關例子可以查看下面倉庫中的chapter3-9目錄:

  • Github:https://github.com/dyc87112/SpringBoot-Learning/
  • Gitee:https://gitee.com/didispace/SpringBoot-Learning/

如果您覺得本文不錯,歡迎Star支持,您的關注是我堅持的動力!

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

新北清潔公司,居家、辦公、裝潢細清專業服務

※推薦評價好的iphone維修中心

Dubbo想要個網關怎麼辦?試試整合Spring Cloud Gateway

一、背景

在微服務架構中 API網關 非常重要,網關作為全局流量入口並不單單是一個反向路由,更多的是把各個邊緣服務(Web層)的各種共性需求抽取出來放在一個公共的“服務”(網關)中實現,例如安全認證、權限控制、限流熔斷、監控、跨域處理、聚合API文檔等公共功能。

 

在以 Dubbo 框架體系來構建的微服務架構下想要增加API網關,如果不想自研開發的情況下在目前的開源社區中幾乎沒有找到支持dubbo協議的主流網關,但是 Spring Cloud 體系下卻有兩個非常熱門的開源API網關可以選擇;本文主要介紹如何通過 Nacos 整合 Spring Cloud GatewayDubbo 服務

 

二、傳統 dubbo 架構

dubbo屬於rpc調用,所以必須提供一個web層的服務作為http入口給客戶端調用,並在上面提供安全認證等基礎功能,而web層前面對接Nginx等反向代理用於統一入口和負載均衡。

web層一般是根據業務模塊來切分的,用於聚合某個業務模塊所依賴的各個service服務

PS:我們能否把上圖中的web層全部整合在一起成為一個API網關呢?(不建議這樣做)

因為這樣的web層並沒有實現 泛化調用 必須引入所有dubbo服務的api依賴,會使得網關變得非常不穩定,任何服務的接口變更都需要修改網關中的api依賴!

 

三、整合 Srping Cloud Gateway 網關

下面就開始聊聊直接拿熱門的 Srping Cloud Gateway 來作為dubbo架構體系的網關是否可行,首先該API網關是屬於 Spring Cloud 體系下的組件之一,要整合dubbo的話需要解決以下問題:

  1. 打通註冊中心:spring cloud gateway 需要通過註冊中心發現下游服務,而 dubbo 也需要通過註冊中心實現服務的註冊與發現,如果兩者的註冊中心不能打通的話就會變成雙註冊中心架構就非常複雜了!
  2. 協議轉換: gateway 使用http傳輸協議調用下游服務,而dubbo服務默認使用的是tcp傳輸協議

上面提到的第一個問題“打通註冊中心”其實已經不是問題了,目前dubbo支持 ZookeeperNacos 兩個註冊中心,而 Spring Cloud 自從把 @EnableEurekaClient 改為 @EnableDiscoveryClient 之後已經基本上支持所有主流的註冊中心了,本文將使用 Nacos 作為註冊中心打通兩者

 

3.1. 方式一

把傳統dubbo架構中的 Nginx 替換為 Spring Cloud Gateway ,並把 安全認證 等共性功能前移至網關處實現

由於web層服務本身提供的就是http接口,所以網關層無需作協議轉換,但是由於 安全認證 前移至網關了需要通過網絡隔離的手段防止被繞過網關直接請求後面的web層

 

3.2. 方式二

dubbo服務本身修改或添加 rest 傳輸協議的支持,這樣網關就可以通過http傳輸協議與dubbo服務通信了

rest傳輸協議:基於標準的Java REST API——JAX-RS 2.0(Java API for RESTful Web Services的簡寫)實現的REST調用支持

目前版本的dubbo已經支持dubbo、rest、rmi、hessian、http、webservice、thrift、redis等10種傳輸協議了,並且還支持同一個服務同時定義多種協議,例如配置 protocol = { “dubbo”, “rest” } 則該服務同時支持 dubborest 兩種傳輸協議

 

3.3. 總結

方式一 對比 方式二 多了一層web服務所以多了一次網絡調用開銷,但是優點是各自的職責明確單一,web層可以作為聚合層用於聚合多個service服務的結果經過融合加工一併返回給前端,所以這種架構下能大大減少服務的 循環依賴

 

四、代碼實踐

依賴環境

  • lombok
  • jdk 1.8
  • Nacos 1.3
  • Spring Boot 2.2.8.RELEASE
  • Spring Cloud Hoxton.SR5
  • Spring Cloud Alibaba 2.2.1.RELEASE

 

在根目錄的 pom.xml 中定義全局的依賴版本

    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>8</java.version>

        <spring-boot-dependencies.version>2.2.8.RELEASE</spring-boot-dependencies.version>
        <spring-cloud-dependencies.version>Hoxton.SR5</spring-cloud-dependencies.version>
        <spring-cloud-alibaba-dependencies.version>2.2.1.RELEASE</spring-cloud-alibaba-dependencies.version>
        <jaxrs.version>3.12.1.Final</jaxrs.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot-dependencies.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud-dependencies.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring-cloud-alibaba-dependencies.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

 

4.1. 創建dubbo-api工程

分別定義兩個api接口

DubboService 使用dubbo協議的服務

public interface DubboService {
    String test(String param);
}

RestService 使用rest協議的服務

public interface RestService {
    String test(String param);
}

 

4.2. 創建web-dubbo工程

使用 方式一 整合對接網關,這裏為了簡化在同一個服務下只使用邏輯分層定義controller層與service層,並沒有做服務拆分

4.2.1. 創建配置

定義 spring boot 配置

server:
  port: 8081

spring:
  application:
    name: zlt-web-dubbo
  main:
    allow-bean-definition-overriding: true
  cloud:
    nacos:
      server-addr: 192.168.28.130:8848
      username: nacos
      password: nacos

server.port:配置應用服務器暴露的端口

spring.cloud.nacos:配置 spring cloud 的註冊中心相關參數,nacos 的配置需要改為自己環境所對應

定義 dubbo 配置

dubbo:
  scan:
    base-packages: org.zlt.service
  protocols:
    dubbo:
      name: dubbo
      port: -1
  registry:
    address: spring-cloud://localhost
  consumer:
    timeout: 5000
    check: false
    retries: 0
  cloud:
    subscribed-services:

dubbo.scan.base-packages:指定 Dubbo 服務實現類的掃描基準包

dubbo.protocols:服務暴露的協議配置,其中子屬性 name 為協議名稱,port 為協議端口( -1 表示自增端口,從 20880 開始)

dubbo.registry.address:Dubbo 服務註冊中心配置,其中子屬性 address 的值 “spring-cloud://localhost”,說明掛載到 Spring Cloud 註冊中心

 

4.2.2. 創建DubboService的實現類

通過 protocol = "dubbo" 指定使用 dubbo協議 定義服務

@Service(protocol = "dubbo")
public class DubboServiceImpl implements DubboService {
    @Override
    public String test(String param) {
        return "dubbo service: " + param;
    }
}

 

4.2.3. 創建Controller類

使用 Spring Boot@RestController 註解定義web服務

@RestController
public class WebController {
    @Autowired
    private DubboService dubboService;

    @GetMapping("/test/{p}")
    public String test(@PathVariable("p") String param) {
        return dubboService.test(param);
    }
}

 

4.3. 創建rest-dubbo工程

使用 方式二 整合對接網關,由於該服務是通過dubbo來創建rest服務,所以並不需要使用 Spring Boot 內置應用服務

4.3.1. 創建配置

定義 spring boot 配置

spring:
  application:
    name: zlt-rest-dubbo
  main:
    allow-bean-definition-overriding: true
  cloud:
    nacos:
      server-addr: 192.168.28.130:8848
      username: nacos
      password: nacos

因為不使用 Spring Boot 內置的應用服務所以這裏並不需要指定 server.port

定義 dubbo 配置

dubbo:
  scan:
    base-packages: org.zlt.service
  protocols:
    dubbo:
      name: dubbo
      port: -1
    rest:
      name: rest
      port: 8080
      server: netty
  registry:
    address: spring-cloud://localhost
  consumer:
    timeout: 5000
    check: false
    retries: 0
  cloud:
    subscribed-services:

dubbo.protocols:配置兩種協議,其中rest協議定義 8080 端口並使用 netty 作為應用服務器

 

4.3.2. 創建RestService的實現類

通過 protocol = "rest" 指定使用 rest協議 定義服務

@Service(protocol = "rest")
@Path("/")
public class RestServiceImpl implements RestService {
    @Override
    @Path("test/{p}")
    @GET
    public String test(@PathParam("p") String param) {
        return "rest service: " + param;
    }
}

 

4.4. 創建Spring Cloud Gateway工程

定義 spring boot 配置

server:
  port: 9900

spring:
  application:
    name: sc-gateway
  main:
    allow-bean-definition-overriding: true
  cloud:
    nacos:
      server-addr: 192.168.28.130:8848
      username: nacos
      password: nacos

server.port:定義網關端口為 9090

定義網關配置

spring:
  cloud:
    gateway:
      discovery:
        locator:
          lowerCaseServiceId: true
          enabled: true
      routes:
        - id: web
          uri: lb://zlt-web-dubbo
          predicates:
            - Path=/api-web/**
          filters:
            - StripPrefix=1
        - id: rest
          uri: lb://zlt-rest-dubbo
          predicates:
            - Path=/api-rest/**
          filters:
            - StripPrefix=1

分別定義兩個路由策略:

  • 路徑 /api-web/ 為請求 web-dubbo 工程
  • 路徑 /api-rest/ 為請求 rest-dubbo 工程

 

4.5. 測試

分別啟動:Nacos、sc-gateway、web-dubbo、rest-dubbo 工程,通過網關的以下兩個接口分別測試兩種整合方式

  1. http://127.0.0.1:9900/api-web/test/abc :請求 web-dubbo 工程測試整合方式一
  2. http://127.0.0.1:9900/api-rest/test/abc :請求 rest-dubbo 工程測試整合方式二

 

五、demo下載

ide需要安裝 lombok 插件

https://github.com/zlt2000/dubboSpringCloud

 
掃碼關注有驚喜!

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

邏輯式編程語言極簡實現(使用C#) – 1. 邏輯式編程語言介紹

相信很多朋友對於邏輯式編程語言,都有一種最熟悉的陌生人的感覺。一方面,平時在書籍、在資訊網站,偶爾能看到一些吹噓邏輯式編程的話語。但另一方面,也沒見過周圍有人真正用到它(除了SQL)。

遙記當時看《The Reasoned Schemer》(一本講邏輯式編程語言的小人書),被最後兩頁的解釋器實現驚艷到了。看似如此複雜的計算邏輯,其實現竟然這麼簡潔。不過礙於當時水平有限,也就囫圇吞棗般看了過去。後來有一天,不知何故腦子靈光一閃,把圖遍歷和流計算模式聯繫在一起,瞬間明白了《The Reasoned Schemer》中的做法。動手寫了寫代碼,果然如此,短短兩百來行代碼,就完成了解釋器的實現,才發現原來如此簡單。很多時候,並非問題本身有多難,只是沒有想到正確的方法。

本系列將盡可能簡潔地說明邏輯式編程語音的原理,並實現一門簡單的邏輯式編程語言。考慮到C#的用戶較多,因此選擇用C#來實現。實現的這門語言就叫NMiniKanren。文章總體內容如下:

  • NMiniKanren語言介紹
    • 語言基礎
    • 一道有趣的邏輯題:誰是兇手
  • NMiniKanren運行原理
    • 構造條件關係圖,遍歷分支
    • 代入消元法解未知量
  • 實現NMiniKanren
    • 流計算模式簡介
    • 代入消元法的實現
    • 遍歷分支的實現

故事從兩個正在吃午餐的程序員說起。

老明和小皮是就職於同一家傳統企業的程序員。這天,兩人吃着午餐。老明邊吃邊刷着抖音,鼻孔時不時噴出幾條米粉。

小皮是一臉麻木地刷着求職網和資訊網,忽然幾個大字映入眼底:《新型邏輯式編程語言重磅出世,即將顛覆IT界!》小皮一陣好奇,往下一翻,結果接着的是一些難懂的話,什麼“一階邏輯”,什麼“合一算法”,以及鬼畫符似的公式之類。

小皮看得索然無味,但被勾引起來的對邏輯式編程的興趣彷彿澳洲森林大火一樣難以平息。於是伸手拍下老明高举手機的左手,問道:“嘿!邏輯式編程有了解過么?是個啥玩意兒?”

“邏輯式編程啊……嘿嘿,前段時間剛好稍微了解了一下。”老明鼻孔朝天吸了兩口氣,“我說的稍微了解,是指實現了一門邏輯式編程語言。”

“不愧是資深老IT,了解也比別人深入一坨坨……”

“也就比你早來一年好不好……我是一邊看一本奇書一邊做的。Dan老師(Dan Friedman)寫的《The Reasoned Schemer》。這本書挺值得一看的,書中使用一門教學用的邏輯式編程語言,講解這門語言的特性、用法、以及原理。最後還給出了這門語言的實現。核心代碼只用了兩頁紙。

“所謂邏輯式編程,從使用上看是把聲明式編程發揮到極致的一種編程範式。普通的編程語言,大部分還是基於命令式編程,需要你告訴機器每一步執行什麼指令。而邏輯式編程的理念是,我們只需要告訴機器我們需要的目標,機器會根據這個目標自動探索執行過程。

邏輯式編程的特點是可以反向運行。你可以像做數學題一樣,聲明未知量,列出方程,然後程序會為你求解未知量。

“挺神奇的。聽起來有點像AI編程。不過這麼高級的東西怎麼沒有流行起來?感覺可以節省不少人力。”小皮忽然有種飯碗即將不保的感覺。

“嘿嘿……想得美。其實邏輯式編程,既不智能,也不好用。你回憶一下你中學的時候是怎麼解方程組的?”

“嗯……先盯一會方程組,看看它長得像不像有快捷解法的樣子。看不出來的話就用代入法慢慢算。這和邏輯式編程有什麼關係?”

邏輯式編程並不智能,它只是把某種類似代入法的通用算法內置到解釋器里。邏輯式編程語言寫的程序運行時,不過是根據通用算法進行求解而已。它不像人一樣會去尋找更快捷的方法,同時也不能解決超綱問題。

而且邏輯式編程語言的學習成本也不低。如果你要用好這門語言,你得把它使用的通用算法搞清楚。雖然你寫的聲明式的代碼,但內心要時刻清楚程序的執行過程。如果你拿它當個黑盒來用,那很可能你寫出來的程序的執行效率會非常低,甚至跑出一些莫名其妙的結果。”

“哦哦,要學會用它,還得先懂得怎麼實現它。這學習成本還挺高的。”小皮跟着吐槽,不過他知道老明表明上看似嫌棄邏輯式編程的實用性,私底下肯定玩得不亦樂乎,並且也喜歡跟別人分享。於是小皮接着道:“雖然應該是用不着,但感覺挺有意思的,再仔細講講唄。天天寫CRUD,腦子都淡出個鳥了。”

果然老明坐直起來:“《The Reasoned Schemer》用的這門邏輯式編程語言叫miniKanren,用Scheme/Lisp實現的。去年給你安利過Scheme了,現在掌握得怎麼樣?”

“一竅不通……”小皮大窘。去年到現在,小皮一直很忙,並沒有自學什麼東西。如果沒有外力驅動的話,他還將一直忙下去。

“果然如此。所以我順手也實現了個C#魔改版本的miniKanren。就叫NMiniKanren。我把NMiniKanren實現為C#的一個DSL。這樣的好處是方便熟悉C#或者Java的人快速上手;壞處是DSL會受限於C#語言的能力,代碼看起來沒有Scheme版那麼優雅。”老明用左手做了個打引號的動作,“先從簡單的例子開始吧。比如說,有個未知量q,我們的目標是讓q等於5或者等於6。那麼滿足條件的q值有哪些?”

“不就是5和6么……這也太簡單了吧。”

“Bingo!”老明打了個響指,“我們先用簡單的例子看看代碼結構。”只見老明兩指輕輕夾住一隻筷子,勾出幾條米粉,快速在桌上擺出如下代碼:

// k提供NMiniKanren的方法,q是待求解的未知變量。
var res = KRunner.Run(null /* null表示輸出所有可能的結果 */, (k, q) =>
{
    // q == 5 或者 q == 6
    return k.Any(
        k.Eq(q, 5),
        k.Eq(q, 6));
});
KRunner.PrintResult(res);  // 輸出結果:[5, 6]

“代碼中,KRunner.Run用於運行一段NMiniKanren代碼,它的聲明如下。”老明繼續撥動米粉:

public class KRunner
{
    public static IList<object> Run(int? n, Func<KRunner, FreshVariable, Goal> body)
    {
        ...
    }
}

“其中,參數n是返回結果的數量限制,n = null表示無限制;參數body是一個函數:

  • 函數的第一個參數是一個KRunner實例,用於引用NMiniKanren方法;
  • 函數的第二個參數是我們將要求解的未知量;
  • 函數的函數體是我們編寫的NMiniKanren代碼;
  • 函數的返回值為需要滿足的約束條件。

“接着我們看函數體的代碼。k.Eq(q, 5)表示q需要等於5k.Eq(q, 6)表示q需要等於6k.Any表示滿足至少一個條件。整段代碼的意思為:求所有滿足q等於5或者q等於6q值。顯然答案為56,程序的運行結果也是如此。很神奇吧?”

“你這米粉打碼的功夫更讓我驚奇……”小皮仔細看了一會,“原來如此。不過這DSL的語法確實看着比較累。”

“主要是我想做得簡單一些。其實使用C#的Lambda表達式也可以實現像……”老明勾出幾條米粉擺出q == 5 || q == 6表達式,“……這樣的語法,不過這樣會增加NMiniKanren實現的複雜度。況且這無非是前綴表達式或中綴表達式這種語法層面的差別而已,語義上並沒有變化。學習應先抓住重點,花里胡哨的東西可以放到最後再來琢磨。

“嗯嗯。KRunner.Run里這個null的參數是做什麼用的呢?”

KRunner.Run的第一個參數用來限制輸出結果的數量。null表示輸出所有可能的結果。還是上面例子的條件,我們改成限制只輸出1個結果。”小皮用筷子改了下代碼:

// k提供NMiniKanren的方法,q是待求解的未知變量。
var res = KRunner.Run(1 /* 輸出1個結果 */, (k, q) =>
{
    // q == 5 或者 q == 6
    return k.Any(
        k.Eq(q, 5),
        k.Eq(q, 6));
});
KRunner.PrintResult(res);  // 輸出結果:[5]

“這樣程序只會輸出5一個結果。在一些包含遞歸的代碼中,可能會有無窮多個結果,這種情況下需要限制輸出結果的數量來避免程序不會終止。”

“原來如此。不過這個例子太簡單了,有沒有其他更好玩的例子。”

老明喝下一口湯,說:“好。時間不早了,我們回公司找個會議室慢慢說。”

NMiniKanren支持的數據類型

到公司后,老明的講課開始了……

首先,要先明確NMiniKanren支持的數據類型。後續代碼都要基於數據類型來編寫,所以規定好數據類型是基礎中的基礎。

簡單起見,NMiniKanren只支持四種數據類型:

  • string:就是一個普普通通的值類型,僅有值相等判斷。
  • int:同string。使用int是因為有時候想少寫兩個雙引號……
  • KPair:二元組。可用來構造鏈表及其他複雜的數據結構。如果你學過Lisp會對這個數據結構很熟悉。下面詳細說明。
  • null:這個類型只有null一個值。表示空引用或者空數組。

KPair類型

KPair的定義為:

public class KPair
{
    public object Lhs { get; set; }
    public object Rhs { get; set; }
    
    // methods
    ...
}

KPair除了用作二元組(其實是最少用的)外,更多的是用來構造鏈表。構造鏈表時,約定一個KPair作為一個鏈表的節點,Lhs為元素值,Rhs為一下個節點。當Rhsnull時鏈表結束。空鏈表用null表示。

public static KPair List(IEnumerable<object> lst)
{
    var fst = lst.FirstOrDefault();
    if (fst == null)
    {
        return null;
    }
    return new KPair(fst, List(lst.Skip(1)));
}

使用null表示空鏈表其實並不合適,這裏純粹是為了簡單而偷了個懶。

我們知道,很多複雜的數據結構都是可以通過鏈表來構造的。所以雖然NMiniKanren只有三種數據類型,但可以表達很多數據結構了。

這時候小皮有疑問了:“C#本身已經自帶了List等容器了,為什麼還要用KPair來構造鏈表?”

“為了讓底層盡可能簡潔。”老明說道,“我們都知道,程序本質上分為數據結構和算法。算法是順着數據結構來實現的。簡潔的數據結構會讓算法的實現顯得更清晰。相比C#自帶的List,使用KPair構造的鏈表更加清晰簡潔。按照構造的方式,我們的鏈表定義為:

  1. 空鏈表null
  2. 或者是非空鏈表。它的第一個元素為Lhs,並且Rhs是後續的鏈表。

“鏈表相關的算法都會順着定義的這兩個分支實現:一個處理空鏈表的分支,一個處理非空鏈表的遞歸代碼。比如說判斷一個變量是不是鏈表的方法:

public static bool IsList(object o)
{
    // 空鏈表
    if (o == null)
    {
        return true;
    }
    // 非空鏈表
    if (o is KPair p)
    {
        // 遞歸
        return IsList(p.Rhs);
    }
    // 非鏈表
    return false;
}

“以及判斷一個元素是不是在鏈表中的方法:

public static bool Memeber(object lst, object e)
{
    // 空鏈表
    if (lst == null)
    {
        return false;
    }
    // 非空鏈表
    if (lst is KPair p)
    {
        if (p.Lhs == null && e == null || p.Lhs.Equals(e))
        {
            return true;
        }
        else
        {
            // 遞歸
            return Memeber(p.Rhs, e);
        }
    }
    // 非鏈表
    return false;
}

“數據類型明確后,接下來我們來看看NMiniKanren能做什麼。”

目標(Goal)

編寫NMiniKanren代碼是一個構造目標(Goal類型)的過程。NMiniKanren解釋器運行時將求解使得目標成立的所有未知量的值

顯然,有兩個平凡的目標:

  • k.Succeed:永遠成立,未知量可取任意值。
  • k.Fail:永遠不成立,無論未知量為何值都不成立。

其中kKRunner的一個實例。C#跟Java一樣不能定義獨立的函數和常量,所以我們DSL需要的函數和常量就都定義為KRunner的方法或屬性。後面不再對k進行複述。

一個基本的目標是k.Eq(v1, v2)。這也是NMiniKanren唯一一個使用值來構造的目標,它表示值v1v2應該相等。也就是說,當v1v2相等時,目標k.Eq(v1, v2)成立;否則不成立。

這裏的相等,指的是值相等:

  • 不同類型不相等。
  • string類型相等當且僅當值相等。
  • KPair類型相等當且僅當它們的Lhs相等且Rhs相等。

KPair相等的定義,可以推出由KPair構造的數據結構(比如鏈表),相等條件為當且僅當它們結構一樣且對應的值相等。

接下來我們看幾個例子。

等於一個值

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    return k.Eq(q, 5);
}));  // 輸出[5]

直接q等於5

等於一個鏈表

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    return k.Eq(q, k.List(1, 2));
}));  // 輸出[(1 2)]

k.List(1, 2)相當於new KPair(1, new KPair(2, null)),用來快速構造鏈表。

鏈表間的相等

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    return k.Eq(k.List(1, q), k.List(1, 2));
}));  // 輸出[2]

這個例子比較像一個方程了。q匹配k.List(1, 2)的第二項,也就是2

無法相等的例子

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    return k.Eq(k.List(2, q), k.List(1, 2));
}));  // 輸出[]

由於k.List(2, q)的第一項和k.List(1, 2)的第一項不相等,所以這個目標無法成立,q沒有值。

不成立的例子

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    return k.Fail;
}));  // 輸出[]

目標無法成立,q沒有值。

永遠成立的例子

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    return k.Succeed;
}));  // 輸出[_0]

目標恆成立,q可取任意值。輸出_0表示一個可取任意值的自由變量。

更多構造目標的方式

目標可以看作布爾表達式,因此可以通過“與或非”運算,用簡單的目標構造成複雜的“組合”目標。我們把被用來構造“組合”目標的目標叫做該“組合”目標的子目標。

定義未知量

在前面的例子中,我們只有一個未知量qq既是未知量,也是程序輸出。

在處理更複雜的問題時,通常需要定義更多的未知量。定義未知量的方法是k.Fresh

// 定義x, y兩個未知量
var x = k.Fresh()
var y = k.Fresh()

新定義的未知量和q一樣,可以用來構造目標:

// x == 2
k.Eq(x, 2)
// x == y
k.Eq(x, y)

使用“與”運算組合的目標,僅當所有子目標成立時,目標才成立。

使用方法k.All來構造“與”運算組合的目標。

var g = k.All(g1, g2, g3, ...)

當且僅當g1, g2, g3, ……,都成立時,g才成立。

特別的,空子目標的情況,即k.All(),恆成立。

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    return k.All(
        k.Eq(q, 1),
        k.Eq(q, 2));
}));  // 輸出[]

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    var x = k.Fresh();
    var y = k.Fresh();
    return k.All(
        k.Eq(x, 1),
        k.Eq(y, x),
        k.Eq(q, k.List(x, y)));
}));  // 輸出[(1 1)]

使用“或”運算組合的目標,只要一個子目標成立時,目標就成立。

使用方法k.Any來構造“或”運算組合的目標。

var g = k.Any(g1, g2, g3, ...)

g1, g2, g3, ……中至少一個成立,g成立。

特別的,空子目標的情況,即k.Any(),恆不成立。

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    return k.Any(
        k.Eq(q, 5),
        k.Eq(q, 6));
}));  // 輸出[5, 6]

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    var x = k.Fresh();
    var y = k.Fresh();
    return k.All(
        k.Any(k.Eq(x, 5), k.Eq(y, 6)),
        k.Eq(q, k.List(x, y)));
}));  // 輸出[(5 _0), (_0 6)]

非?

MiniKanren(以及NMiniKanren)不支持“非”運算。支持“非”會讓miniKanren的實現複雜很多。

這或許令人驚訝。“與或非”在邏輯代數中一直像是連體嬰兒似的扎堆出現。並且“非”運算是單目運算符,看起來應該更簡單。

然而,“與”和“或”運算是在已知的兩(多)個集合中取交集或者並集,結果也是已知的。而“非”運算則是把一個已知的集合映射到可能未知的集合,遍歷“非”運算的結果可能會很久或者就是不可能的。

對於基於圖搜索和代入法求解的miniKanren來說,支持“非”運算需要對核心的數據結構和算法做較大改變。因此以教學為目的的miniKanren沒有支持“非”運算。

不過,在一定程度上,也是有不完整替代方法的。

If(這個比較奇葩,可以先跳過)

If是一個特殊的構造目標的方式。對應《The Reasoned Schemer》中的conda

var g = k.If(g1, g2, g3)

如果g1g2成立,那麼g成立;否則當且僅當g3成立時,g成立。

這個和k.Any(k.All(g1, g2), g3)很像,但他們是有區別的:

  • k.Any(k.All(g1, g2), g3)會解出所有讓k.All(g1, g2)或者g3成立的解
  • k.If(g1, g2, g3)如果k.All(g1, g2)有解,那麼只給出使k.All(g1, g2)成立的解;否則再求使得g3成立的解。

也可以說,If是短路的。

這麼詭異的特性有什麼用呢?

它可以部分地實現“非”運算的功能:

k.If(g, k.Fail, k.Succeed)

這個這裏先不詳細展開了,後面用到再說。

控制輸出順序

這是一個容易被忽略的問題。如果程序需要求出所有的解,那麼輸出順序影響不大。但是一些情況下,求解速度很慢,或者解的數量太多甚至無窮,這時只求前幾個解,那麼輸出的內容就和輸出順序有關了。

因為miniKanren以圖遍歷的方式來查找問題的解,所以解的順序其實也是解釋器運行時遍歷的順序。先看如下例子:

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    var x = k.Fresh();
    var y = k.Fresh();
    return k.All(
        k.Any(k.Eq(x, 1), k.Eq(x, 2)),
        k.Any(k.Eq(y, "a"), k.Eq(y, "b")),
        k.Eq(q, k.List(x, y)));
}));  // 輸出[(1 a), (1 b), (2 a), (2 b)]

有兩個未知變量xyx可能的取值為1或2,y可能的取值為a或b。可以看到,程序查找解的順序為:

  • x值為1
    • y值為a,q=(1 a)
    • y值為b,q=(1 b)
  • x值為2
    • y值為a,q=(2 a)
    • y值為b,q=(2 b)

如果要改變這個順序,我們有一個交替版的“與”運算k.Alli

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    var x = k.Fresh();
    var y = k.Fresh();
    return k.Alli(
        k.Any(k.Eq(x, 1), k.Eq(x, 2)),
        k.Any(k.Eq(y, "a"), k.Eq(y, "b")),
        k.Eq(q, k.List(x, y)));
}));  // 輸出[(1 a), (2 a), (1 b), (2 b)]

不過這個交替版也不是交替得很漂亮。下面增加x可能的取值到3個:

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    var x = k.Fresh();
    var y = k.Fresh();
    return k.Alli(
        k.Any(k.Eq(x, 1), k.Eq(x, 2), k.Eq(x, 3)),
        k.Any(k.Eq(y, "a"), k.Eq(y, "b")),
        k.Eq(q, k.List(x, y)));
}));  // 輸出[(1 a), (2 a), (1 b), (3 a), (2 b), (3 b)]

同樣,“或”運算也有交替版。

正常版:

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    return k.Any(
        k.Any(k.Eq(q, 1), k.Eq(q, 2)),
        k.Any(k.Eq(q, 3), k.Eq(q, 4)));
}));  // 輸出[1, 2, 3, 4]

交替版:

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    return k.Anyi(
        k.Any(k.Eq(q, 1), k.Eq(q, 2)),
        k.Any(k.Eq(q, 3), k.Eq(q, 4)));
}));  // 輸出[1, 3, 2, 4]

後面講到miniKanren實現原理時會解釋正常版、交替版為什麼會是這種表現。

遞歸

無遞歸,不編程!

遞歸給予了程序語言無限的可能。NMiniKanren也是支持遞歸的。下面我們實現一個方法,這個方法構造的目標要求指定的值或者未知量是一個所有元素都為1的鏈表。

錯誤的示範

一個值或者未知量的元素都為1,用遞歸的方式表達是:

  1. 它是一個空鏈表
  2. 或者它的第一個元素是1,且剩餘部分的元素都為1

直譯為代碼就是:

public static Goal AllOne_Wrong(this KRunner k, object lst)
{
    var d = k.Fresh();
    return k.Any(
        // 空鏈表
        k.Eq(lst, null),
        // 非空
        k.All(
            k.Eq(lst, k.Pair(1, d)),  // 第一個元素是1
            k.AllOne_Wrong(d)));  // 剩餘部分的元素都是1
}

直接運行這段代碼,死循環。

為什麼呢?因為我們直接使用C#的方法來定義函數,C#在構造目標的時候,會運行最後一行的k.AllOne_Wrong(d),於是就陷入死循環了。

正確的做法

為了避免死循環,在遞歸調用的地方,需要用k.Recurse方法特殊處理一下,讓遞歸的部分變為惰性求值,防止直接調用:

public static Goal AllOne(this KRunner k, object lst)
{
    var d = k.Fresh();
    return k.Any(
        k.Eq(lst, null),
        k.All(
            k.Eq(lst, k.Pair(1, d)),
            k.Recurse(() => k.AllOne(d))));
}

隨便構造兩個問題運行一下:

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    var x = k.Fresh();
    var y = k.Fresh();
    return k.All(
        k.AllOne(k.List(1, x, y, 1)),
        k.Eq(q, k.List(x, y)));
}));  // 輸出[(1 1)]

KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
    var x = k.Fresh();
    var y = k.Fresh();
    return k.All(
        k.AllOne(k.List(1, x, y, 0)),
        k.Eq(q, k.List(x, y)));
}));  // 輸出[]

k.Recurse這種處理方法其實是比較醜陋而且不好用的。特別是多個函數相互調用引起遞歸的情況,很可能會漏寫k.Recurse導致死循環。

聽到這裏,小皮疑惑道:“這個有點丑誒。剛剛網上瞄了下《The Reasoned Schemer》,發現人家的遞歸併不需要這種特殊處理。看起來直接調用就OK了,跟普通程序沒啥兩樣,很美很和諧。”

“因為《The Reasoned Schemer》使用Lisp的宏實現的miniKanren,宏的機制會有類似惰性計算的效果。”老明用擦白板的抹布拍了下小皮的腦袋,“可惜你不會Lisp。如果你不努力提升自己,那丑一點也只能將就着看了。”

關於數值計算

MiniKanren沒有直接支持數值計算。也就是說,miniKanren不能直接幫你解像2 + x = 5的這種方程。如果要直接支持數值計算,需要實現很多數學相關的運算和變換,會讓miniKanren的實現變得非常複雜。MiniKanren是教學性質的語言,只支持了最基本的邏輯判斷功能。

“沒有‘直接’支持。”小皮敏銳地發現了關鍵,“也就是可以間接支持咯?”

“沒錯!你想想,0和1是我們支持的符號,與和或也是我們支持的運算符!”老明興奮起來了。

“二進制?”

“是的!任何一本計算機組成原理教程都會教你怎麼做!這裏就不多說了,你可以自己回去試一下。”

“嗯嗯。我以前這門課學得還不錯,現在還記得大概是先實現半加器和全加器,然後構造加法器和乘法器等。”小皮幹勁十足,從底層開始讓他想起了小時候玩泥巴的樂趣。

“而且用miniKanren實現的不是一般的加法器和乘法器,是可以反向運行的加法器和乘法器。”

“有意思,晚上下班回去就去試試。”小皮真心地說。正如他下班回家躺床上后,就再也不想動彈一樣真心實意。

(注:《The Reasoned Schemer》第7章、第8章會講到相關內容。)

小結

“好了,NMiniKanren語言的介紹就先說到這裏了。”老明拍了拍手,看了看前面的例子,撇了撇嘴,“以C#的DSL方式實現出來果然丑很多,語法上都不一致了。不過核心功能都還在。”

“接下來就是最有意思的部分,NMiniKanren的原理了吧?”

“是的。不過在繼續之前,還有個問題。”

“啥問題?”

“中午米線都用來打碼了。現在肚子餓了,你要請我吃下午茶。”

NMiniKanren的源碼在:https://github.com/sKabYY/NMiniKanren

示例代碼在:https://github.com/sKabYY/NMiniKanren/tree/master/NMiniKaren.Tests

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

新北清潔公司,居家、辦公、裝潢細清專業服務

Java 從入門到進階之路(二十五)

在之前的文章我們介紹了一下 Java 中的  集合框架中的Collection 的子接口 List的 增刪改查和與數組間相互轉換的方法,本章我們來看一下 Java 集合框架中的Collection 的子接口 List 的另外一些方法。

我們在使用集合的時候難免會對其中的元素進行排序,因為 Set 集合本身是無序的,所以本章將着重講解 List 集合,如下:

 1 import java.util.ArrayList;
 2 import java.util.Collections;
 3 import java.util.List;
 4 import java.util.Random;
 5 
 6 /**
 7  * 排序集合元素
 8  * 排序集合使用的是集合的工具類 Collections 的靜態方法 sort
 9  * 排序僅能對 List 集合進行,因為 Set 部分實現類是無序的
10  * */
11 public class Main {
12     public static void main(String[] args) {
13         List<Integer> list = new ArrayList<Integer>();
14         Random random = new Random();
15         for(int i=0;i<10;i++){
16             list.add(random.nextInt(100));
17         }
18         System.out.println(list); // [49, 24, 29, 59, 56, 1, 1, 5, 49, 60]
19 
20         Collections.sort(list);
21         System.out.println(list); // [1, 1, 5, 24, 29, 49, 49, 56, 59, 60]
22     }
23 }

在上面的代碼中,我們隨機生成了一些正數並添加到 List 集合中,我們通過 sort 方法,系統變自動按照自然數的排列方法為我們做好了排序,很是方便,那如果 List 中使我們自己定義的內容,比如說一個類,那該如何排序呢,如下:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * 排序集合元素
 * 排序集合使用的是集合的工具類 Collections 的靜態方法 sort
 * 排序僅能對 List 集合進行,因為 Set 部分實現類是無序的
 */
public class Main {
    public static void main(String[] args) {
        List<Point> list = new ArrayList<Point>();
        list.add(new Point(1, 2));
        list.add(new Point(2, 3));
        list.add(new Point(4, 2));
        list.add(new Point(2, 5));
        list.add(new Point(9, 3));
        list.add(new Point(7, 1));
        System.out.println(list); // [(1, 2), (2, 3), (4, 2), (2, 5), (9, 3), (7, 1)]
        // Collections.sort(list); // 編譯錯誤,不知道 Point 排序規則
        /**
         * sort 方法要求集合必須實現 Comparable 接口
         * 該接口用於規定實現類事可以比較的
         * 其中一個 compareTo 方法時用來定義比較大小的規則
         * */
        Collections.sort(list);
        System.out.println(list); // [(1, 2), (2, 3), (4, 2), (2, 5), (7, 1), (9, 3)]

    }
}

class Point implements Comparable<Point> { // 定義為泛型 T 類型
    private int x;
    private int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public void setX(int x) {
        this.x = x;
    }

    public int getY() {
        return y;
    }

    public void setY(int y) {
        this.y = y;
    }

    @Override
    public String toString() {
        return "(" + x + ", " + y + ")";
    }

    /**
     * 當實現了 Comparable 接口后,需要重寫下面的方法
     * 該方法的作用是定義當前對象與給定參數對象比較大小的規則
     * 返回值為一個 int 值,該值表示大小關係
     * 它不關注具體的取值是多少,而關注的是取值範圍
     * 當返回值 >0 時:當前對象不參數對象大
     * 當返回值 <0 時:當前對象比參數對象小
     * 當返回值 =0 時:兩個對象相等
     */
    @Override
    public int compareTo(Point o) {
        /**
         * 比較規則,點到原點的距離長的打
         * */
        int len = this.x * this.x + this.y * this.y;
        int olen = o.x * o.x + o.y * o.y;
        return len - olen;
    }
}

在上面的代碼中,我們跟之前一樣定義了一個 Point 類,然後實例化后存入 list 集合中,如果我們相對 Point 進行排序,如果直接調用 sort 方法會報錯,這是由於編譯器不知道我們定義的 Point 類的排序規則,所以我們需要通過接口 Comparable 接口來自定義排序規則,如下圖:

 

我們可以通過坐標系中點到原點的距離來判斷 Point 的大小,即 x*x+y*y 的大小,然後通過重寫 compareTo 方法來實現 sort 。

實際上雖然能實現,但是並不理想,為了實現一個 sort 方法,要求我們的集合元素必須實現 Comparable 接口並且定義比較規則,這種我們想使用某個功能,而它要求我們修改程序的現象稱為“侵入性”。修改的代碼越多,侵入性越強,越不利於程序的擴展。

我們再來看一下下面的排序:

 1 import java.util.ArrayList;
 2 import java.util.Collections;
 3 import java.util.List;
 4 
 5 public class Main {
 6     public static void main(String[] args) {
 7         List<String> list1 = new ArrayList<String>();
 8         list1.add("Java");
 9         list1.add("c++");
10         list1.add("Python");
11         list1.add("PHP");
12         Collections.sort(list1);
13         System.out.println(list1); // [Java, PHP, Python, c++]
14 
15         List<String> list2 = new ArrayList<String>();
16         list2.add("孫悟空");
17         list2.add("豬八戒");
18         list2.add("唐僧");
19         list2.add("六小齡童");
20         Collections.sort(list2);
21         System.out.println(list2); // [六小齡童, 唐僧, 孫悟空, 豬八戒]
22 
23     }
24 }

在上面的排序中,當我們對字母或漢字進行排序時,其實是比的第一個字的 Unicode 碼,那如果我們不使用 Comparable 的接口該如何自定義我們想要的實現規則呢?比如根據字符串的長度來進行排序,其實 sort 有一個額外的重載方法來實現我們想要的結果,如下:

 1 import java.util.ArrayList;
 2 import java.util.Collections;
 3 import java.util.Comparator;
 4 import java.util.List;
 5 
 6 public class Main {
 7     public static void main(String[] args) {
 8         /**
 9          * 重載的 sort 方法要求傳入一個額外的比較器
10          * 該方法不再要求集合元素必須實現 Comparable 接口
11          * 並且不再使用集合元素資深的比較規則排序了
12          * 而是根據給定的這個額外的比較器的比較規則對集合元素進行排序
13          * 實際開發中也推薦使用這種方式進行集合元素排序
14          * 若集合元素是自定義的
15          * 創建比較器時推薦使用匿名內部類的形式
16          */
17         List<String> list1 = new ArrayList<String>();
18         list1.add("Java");
19         list1.add("c++");
20         list1.add("Python");
21         list1.add("PHP");
22         MyComparator myComparator1 = new MyComparator();
23         Collections.sort(list1, myComparator1); // 重載 sort
24         System.out.println(list1); // [c++, PHP, Java, Python]
25 
26         List<String> list2 = new ArrayList<String>();
27         list2.add("孫悟空");
28         list2.add("豬八戒");
29         list2.add("唐僧");
30         list2.add("六小齡童");
31         // 匿名內部類形式創建
32         Comparator<String> myComparator2 = new Comparator<String>() {
33             @Override
34             public int compare(String o1, String o2) {
35                 return o1.length() - o2.length();
36             }
37         };
38         Collections.sort(list2, myComparator2); // 重載 sort
39         System.out.println(list2); // [唐僧, 孫悟空, 豬八戒, 六小齡童]
40 
41     }
42 }
43 
44 /**
45  * 定義一個額外的比較器
46  * 該方法用來定義 o1 和 o2 的比較
47  * 若返回值 >0:o1>o2
48  * 若返回值 <0:o1<o2
49  * 若返回值 =0:兩個對象相等
50  */
51 class MyComparator implements Comparator<String> {
52     @Override
53     public int compare(String o1, String o2) {
54         /**
55          * 字符串中字符多的大
56          * */
57         return o1.length() - o2.length();
58     }
59 }

    

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※幫你省時又省力,新北清潔一流服務好口碑

※別再煩惱如何寫文案,掌握八大原則!

電動車進駐大樓,ChargePoint 推出公寓大樓用電動車充電座

在美國,許多人都擁有寬敞的車庫,買了電動車就在車庫中設置充電座,然而若是市區中的大樓住戶,只能停在大樓地下室密密麻麻的停車格,買電動車可就麻煩了。電動車充電座製造商 ChargePoint 看見了這個問題,計劃推出公寓大樓用電動車充電座,解除大樓住戶購買電動車的障礙。    
  《富比世》報導,當特斯拉 Model S 電動車上市時,Gogoro 創辦人陸學森原本想要擁有一輛,但是第一個問題是在台灣沒有上市,就算進口一輛,台灣的住處大樓沒有獨立車庫,無法安裝幫 Model S 電動車充電的充電座,若非要開 Model S 電動車,只能在公司裝設充電座在公司充電,但這樣一來,週末就無法開出去兜風,豈不是大煞風景,最後陸學森在女友說服下,還是打退堂鼓。   這個困擾,其實也是各國所有大樓住戶的困擾,雖然以美國來說,如加州等地區路上設有充電站,不過電動車車主總是想要在家把電充飽飽才開出門,以免半路沒電,據美國能源部統計,80% 電動車都是在家充電,要是在家不能充電,購買電動車的意願就會降低,ChargePoint  執行長帕斯奎‧羅曼諾(Pasquale Romano)表示,除了少數例外,住大樓的人通常不買電動車,正是因為如此。   那要如何改善這個情況?羅曼諾認為,過去為了讓少數電動車主能在大樓停車場充電,大樓業主得全數自掏腰包在停車場設置充電座,投資風險很高,因此意願低落,但大樓業主如果並不用負擔充電座的設置費用,像大樓附設的投幣式自助洗衣機一樣,由業者來設置機器,這樣就成了。      
可吸引高收入使用者   2015 年 4 月,ChargePoint 宣布推出大樓專用的充電座系統,大樓業主不用負擔設置費用,這部分完全由 ChargePoint  吸收,大樓業主只需要為充電座連接電力即可,ChargePoint 會向用戶收取每月 39.99 美元的月費,電費部分則由住戶直接交給大樓業主,如果有電動車的住戶搬走了,ChargePoint 可以暫時關閉住戶所屬停車格的充電座,直到下一位有電動車的住戶入住才重新啟動,這樣一來,大樓業主的風險可說降到極低,勢必能提高安裝充電座的意願。   對大樓來說,提供停車場充電座設施,可吸引電動車車主,讓大樓更快租出,電動車車主又通常是高收入、高社經地位的良好住戶,對大樓有額外幫助;而對 ChargePoint 來說,能打進大樓這片處女地,是開拓新市場的絕佳機會,估計至 2020 年,美國將有 230 萬電動車主,其中有 10% 將會住在大樓內,這種合作方式對大樓與 ChargePoint 可說是雙贏局面。   想出免費贈送充電座商業模式的也不只 ChargePoint,曾經推出低價「開源碼」充電座的新創事業 EMotorWerks,2014 年推出特別活動,免費贈送原本售價 299 美元的 JuiceBox 充電座,條件是用戶要有可用的 Wi-Fi,讓充電座能將資訊傳給 EMotorWerks,以及用戶同意可由 EMotorWerks 來調整充電速度。    
 

    用戶只需要在行動裝置的專屬 App 上,告訴 EMotorWerks 何時要用車要充飽電力,EMotorWerks 會根據電力的離峰尖峰情況,自動調整充電速度,盡可能讓電力都在離峰時充電。在加州,尖峰電價可能高出平均電價 30 倍以上,避開尖峰時段充電可以為用戶節省大量電費;另一方面,也相當於為電網平衡離尖峰電力需求,如夜間風力發電過剩,可加速充電把多餘的電力用掉,尖峰時暫停充電,緩和尖峰負載。   EMotorWerks 未來的營收可望來自為用戶節省電費的服務、為電網調節平衡的服務,以及出售所收集的數據,為此,免費贈送充電座也划算。   EMotorWerks 的點子,也可能為 ChargePoint 採用,ChargePoint 充電座也可能成為大樓業主調節電力負載與電費支出的利器。無論如何,在充電座業者的推波助瀾下,電動車的充電障礙,將漸漸減輕。     本文全文授權轉載自《科技新報》─〈〉

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

新北清潔公司,居家、辦公、裝潢細清專業服務

※推薦評價好的iphone維修中心

特斯拉推儲能電池系統 德國石墨陽極商 SGL 受惠

美國豪華電動車製造商特斯拉 (Tesla) 跨界家用電池,德國汽車碳纖維生產商西格里集團 (SGL Carbon SE) 在投資人期待該公司可望因此受惠的激勵下,股價創下近三個月以來最大單日漲幅。   SGL 主要是為日立、Panasonic 這些日本電子大廠供應石墨陽極材料,而日立、Panasonic 則會將電池元件賣給特斯拉。   Bankhaus Lampe 分析師 Marc Gabriel 說,SGL 是少數幾家能因電池需求增溫而受惠的德國業者。根據報導,SGL 發言人已確認,該公司的確是日立、Panasonic 的石墨陽極材料供應商。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

特斯拉Q1財報優 今年或賣出5.5萬輛車

特斯拉6日公佈首季財報,銷售金額比去年同期增加 50%,營收高達 11 億美元,每股淨損 36 美分,優於分析師預期,股價盤後上漲 2.4%,上個月則已累積大漲達 13%。同時,特斯拉預計新的太陽能電池廠將在今年第 3 季啟用,明年第一季前汽車產能估計將快速暴增。   特斯拉首季賣出的車輛達 10045 輛,稍優於原先發佈的 10030 輛,預期 2015 年總銷售車輛將可達 5.5 萬輛。同時,特斯拉的最新車款 Model X 也將於第三季開始販售。特斯拉表示,受到美元強勢走升的影響,今年首季提列的匯損金額高達 2200 萬美元,預計第二季 Model X 的售價仍會受美元影響,因此其將在第三季後調漲部分歐洲市場的電動車售價。   目前特斯拉仍積極在美國擴產,第三季新的太陽能電池廠就即將啟用,汽車產能到了 2016 年首季可望大幅增加。去年馬斯克稱將在內華達州建造一個超級電池工廠,到了 2020 年將生產約 50 萬顆的鋰電池。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

新北清潔公司,居家、辦公、裝潢細清專業服務

刺激銷量?北京6月起收取電動車充電服務費

北京市發改委於5月7日發佈了《關於本市電動汽車充電服務收費有關問題的通知》,從2015年6月1日起,提供電動汽車充電服務的充電設施經營單位在收取電費的同時,可按充電電量額外收取充電服務費,每千瓦時收費上限標準為當日本市92號汽油每升最高零售價的15%。各經營單位可在不超過上限標準情況下,制定具體收費標準。   其充電服務費上限標準隨油價變動自行動態調整。如2015年4月29日本市92號汽油最高零售價為6.46元人民幣(下同)/升,則充電服務收費上限標準為6.46元的15%,即0.97元/度。油價上升,充電服務費相應上升油價下降,充電服務費相應下降。   北京市發改委副主任高朋指出,根據測算,此標準可以確保電動汽車動力成本低於燃油汽車。以本市銷售相對較好的北汽E150EV電動汽車為例(100公里平均耗電16度),當油價在6-10元/升區間變動時,充電服務費為每度電0.9元-1.5元。按國家發展改革委檔,對向電力公司直接報裝的充電服務設施,按大工業用電價格標準執行,按此測算,加上電價費用,電動汽車動力成本約為同款燃油汽車的50%-60%左右。   對此有觀點認為,發改委此舉會影響電動汽車銷量,降低消費者對電動車的購買慾。汽車產業分析師張志勇指出,目前,電動車推廣最大的障礙是充電問題,《通知》中規定了電動車充電服務費,可以鼓勵一些民營資本來加入電動車充電網路的建設,此舉將加快北京地區的充電網路建設。   張志勇還指出,即使充電設施經營單位額外收取充電服務費,這個成本也是遠遠低於燃油車的用車成本,這對消費者來看並不能產生多大影響。因此《通知》發布後不僅不會影響電動車的銷量,反而會增加消費者購買電動車的信心。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※幫你省時又省力,新北清潔一流服務好口碑

※別再煩惱如何寫文案,掌握八大原則!